@centralu/win32-x64 0.1.0-beta.47 → 0.1.0-beta.48

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.
@@ -43050,7 +43050,7 @@ import { dirname as dirname26, join as join59 } from "node:path";
43050
43050
  import { mkdirSync as mkdirSync21, readFileSync as readFileSync27, writeSync as writeSync4 } from "node:fs";
43051
43051
 
43052
43052
  // packages/protocol/src/version.generated.ts
43053
- var APP_VERSION = "0.1.0-beta.47";
43053
+ var APP_VERSION = "0.1.0-beta.48";
43054
43054
 
43055
43055
  // packages/protocol/src/brand.ts
43056
43056
  var APP_NAME = "Centralu";
@@ -57761,7 +57761,8 @@ var SessionState = external_exports.enum([
57761
57761
  "error"
57762
57762
  ]);
57763
57763
  var SessionActivity = external_exports.enum(["compacting", "reviewing", "retrying"]);
57764
- var InterruptionReason = external_exports.enum(["network", "quit", "interrupted", "reconnecting"]).catch("interrupted");
57764
+ var InterruptionReason = external_exports.enum(["network", "quit", "shutdown", "interrupted", "reconnecting"]).catch("interrupted");
57765
+ var SHUTDOWN_WORDS = "the computer shut down, restarted or signed out";
57765
57766
  var Interruption = external_exports.object({
57766
57767
  reason: InterruptionReason,
57767
57768
  /** When it happened (ms); for `reconnecting`, when the reconnecting began */
@@ -59671,6 +59672,8 @@ var TurnWho = external_exports.object({
59671
59672
  by: external_exports.enum(["person", "work"]),
59672
59673
  wrote: external_exports.literal(true).optional(),
59673
59674
  added: external_exports.literal(true).optional(),
59675
+ /** An update of Centralu cut this turn and Centralu continued it (#1966): the report of its end says so */
59676
+ cut: external_exports.literal(true).optional(),
59674
59677
  compaction: external_exports.literal(true).optional(),
59675
59678
  since: external_exports.number().int().optional()
59676
59679
  });
@@ -61807,6 +61810,12 @@ var hostParams = {
61807
61810
  * says it in its hellos and sends `host_handover`, so a hub's row can say it. `handover: null` clears it. Additive
61808
61811
  */
61809
61812
  "host.handover": external_exports.object({ handover: HostHandover.nullable() }),
61813
+ /**
61814
+ * The system is ending (a shutdown, restart or sign-out), told by the window on a machine where the host has no window of its
61815
+ * own to hear it: Windows, whose desktop window runs `centralu serve --window` and cannot write to a host it did not start
61816
+ * (#1970, #1397). `cancelled`: the end announced before was called off. Additive
61817
+ */
61818
+ "host.systemEnding": external_exports.object({ cancelled: external_exports.boolean().optional() }),
61810
61819
  /**
61811
61820
  * A hub linked to this host says who it is and whether this host's sessions may ask its projects (#550, the reverse
61812
61821
  * direction, docs/plans/remote-hub.md §12.7). Sent by the hub at every connect and when its switch for this machine
@@ -61878,6 +61887,7 @@ var hostResults = {
61878
61887
  external_exports.object({ ok: external_exports.literal(false), busy: HostActivity, calls: external_exports.number().int() })
61879
61888
  ]),
61880
61889
  "host.handover": external_exports.object({ ok: external_exports.literal(true) }),
61890
+ "host.systemEnding": external_exports.object({ ok: external_exports.literal(true) }),
61881
61891
  "host.hubOffer": external_exports.object({ ok: external_exports.literal(true) })
61882
61892
  };
61883
61893
 
@@ -63454,6 +63464,12 @@ var settingsParams = {
63454
63464
  * like any other file and gets a fresh id if the name is taken.
63455
63465
  */
63456
63466
  "themes.import": external_exports.object({ path: external_exports.string() }),
63467
+ /**
63468
+ * Takes a theme file out of the folder, without losing it: the file moves to the folder's `.removed/`, where the
63469
+ * theme list does not look (an OS trash is the shell's, `themes.resolve`; this is the host's own, for a caller with no shell, the orchestrator).
63470
+ * A side that showed it falls back to its preset (the caller's to set, as Settings does).
63471
+ */
63472
+ "themes.remove": external_exports.object({ id: ThemeId }),
63457
63473
  /**
63458
63474
  * The absolute path of a theme file, for the shell to trash or reveal (the same two-step as
63459
63475
  * `fs.resolve`: the host knows the folder, the shell knows the OS).
@@ -63474,6 +63490,7 @@ var settingsResults = {
63474
63490
  "themes.list": external_exports.array(ThemeFileEntry),
63475
63491
  "themes.save": ThemeFileEntry,
63476
63492
  "themes.import": ThemeFileEntry,
63493
+ "themes.remove": external_exports.object({ removed: external_exports.boolean(), keptAt: external_exports.string().nullable() }),
63477
63494
  "themes.resolve": external_exports.object({ path: external_exports.string() }),
63478
63495
  "approvals.rules": external_exports.array(
63479
63496
  external_exports.object({
@@ -65587,6 +65604,13 @@ function logonFromGroups(stdout) {
65587
65604
  if (sids.has("S-1-5-6")) return "service";
65588
65605
  return null;
65589
65606
  }
65607
+ function systemStoppingQueries(platform = HOST_PLATFORM) {
65608
+ if (platform !== "linux") return null;
65609
+ return [
65610
+ ["systemctl", "is-system-running"],
65611
+ ["systemctl", "--user", "is-system-running"]
65612
+ ];
65613
+ }
65590
65614
 
65591
65615
  // packages/agent-host/src/dev-services/path-guard.ts
65592
65616
  import { lstatSync as lstatSync2, realpathSync, statSync } from "node:fs";
@@ -66409,6 +66433,8 @@ var METHOD_RIGHTS = {
66409
66433
  * launcher says it with the limited credential and no full one crosses a port for it; `host.stop` does far more
66410
66434
  */
66411
66435
  "host.handover": "everyday",
66436
+ /** The window telling the serve host the system is ending (#1970): said with the window's full credential. An everyday one could plant a false end that relabels a real crash as the system's (#1397) */
66437
+ "host.systemEnding": "window",
66412
66438
  "host.hubOffer": "link",
66413
66439
  // The window's own state, preferences, themes and approval rules
66414
66440
  "workspace.save": "window",
@@ -66421,6 +66447,7 @@ var METHOD_RIGHTS = {
66421
66447
  "themes.list": "everyday",
66422
66448
  "themes.save": "everyday",
66423
66449
  "themes.import": "everyday",
66450
+ "themes.remove": "everyday",
66424
66451
  "themes.resolve": "everyday",
66425
66452
  "approvals.rules": "everyday"
66426
66453
  };
@@ -67544,7 +67571,7 @@ var HostServer = class _HostServer {
67544
67571
  return;
67545
67572
  }
67546
67573
  if (refusal2 !== null) {
67547
- console.error(`[agent-host] refused ${JSON.stringify(method)} to a connection holding the limited credential (${refusal2.rights})`);
67574
+ console.error(`[agent-host] refused ${JSON.stringify(method)} to a connection holding the limited credential (${refusal2.rights})${method === "host.systemEnding" ? ": a launcher older than this host sends the system-ending notice with it, so that end is not noted until the launcher is updated (#1970)" : ""}`);
67548
67575
  return this.sendError(ws2, id3, { code: "internal", message: refusal2.message, retryable: false, data: { refused: "rights", rights: refusal2.rights } });
67549
67576
  }
67550
67577
  }
@@ -106570,6 +106597,8 @@ function matchProjects(asked, projects, o = {}) {
106570
106597
  return [];
106571
106598
  }
106572
106599
  var NO_WORDS = "(its turn ended without saying anything: read_session shows what it did)";
106600
+ var MACHINE_UNREACHABLE = "(not read: its machine could not be reached)";
106601
+ var IN_PAUSE_REPORT = "(in the paused report before this one)";
106573
106602
  function clipAnswer(answer, sessionName, at2) {
106574
106603
  if (answer.length <= ANSWER_MAX_CHARS) return answer;
106575
106604
  const cut3 = answer.length - ANSWER_HEAD_CHARS - ANSWER_TAIL_CHARS;
@@ -106825,9 +106854,9 @@ async function gitStatusFiles(cwd, trust = {}) {
106825
106854
  if (entry.startsWith("1 ") || entry.startsWith("2 ")) {
106826
106855
  const parts = entry.split(" ");
106827
106856
  const xy = parts[1] ?? "..";
106828
- const rename7 = entry.startsWith("2 ");
106829
- const path2 = parts.slice(rename7 ? 9 : 8).join(" ");
106830
- if (rename7) i++;
106857
+ const rename8 = entry.startsWith("2 ");
106858
+ const path2 = parts.slice(rename8 ? 9 : 8).join(" ");
106859
+ if (rename8) i++;
106831
106860
  if (!path2) continue;
106832
106861
  const [x, y] = [xy[0] ?? ".", xy[1] ?? "."];
106833
106862
  if (x !== ".") out.push({ path: path2, staged: true, status: mapStatus(x) });
@@ -108238,6 +108267,264 @@ function givenAgentName(name) {
108238
108267
  return flat ? oneLineText(flat, 40) : void 0;
108239
108268
  }
108240
108269
 
108270
+ // packages/agent-host/src/sessions/handoff-heirs.ts
108271
+ function liveHeirOf(store2, sessionId, isLive) {
108272
+ const seen = /* @__PURE__ */ new Set([sessionId]);
108273
+ let generation = [sessionId];
108274
+ while (generation.length) {
108275
+ const heirs = generation.flatMap((at2) => store2.handoffs.handoffHeirs(at2)).filter((h) => !seen.has(h.sessionId));
108276
+ for (const h of heirs) seen.add(h.sessionId);
108277
+ const live2 = heirs.find((h) => !h.trashed && isLive(h.sessionId));
108278
+ if (live2) return live2.sessionId;
108279
+ generation = heirs.map((h) => h.sessionId);
108280
+ }
108281
+ return null;
108282
+ }
108283
+
108284
+ // packages/agent-host/src/sessions/interruptions.ts
108285
+ var NETWORK_WORDS = [
108286
+ /\b(ECONNREFUSED|ENOTFOUND|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|EHOSTUNREACH|ENETDOWN)\b/,
108287
+ /API Error: (No response from API|Can't reach the API server|Connection (refused|lost|reset|error|dropped)|Request timed out|No internet route|fetch failed|Unable to connect to API(?!: SSL))/i,
108288
+ /computer (went to sleep|was asleep)/i,
108289
+ /stream disconnected before completion/i,
108290
+ /failed to lookup address information/i,
108291
+ /Connection failed: error sending request/i,
108292
+ /waiting for network/i
108293
+ ];
108294
+ function failureSentence(message) {
108295
+ return message.split(/(?:^|[.\s])stderr:/)[0].trim();
108296
+ }
108297
+ function isNetworkFailure(message) {
108298
+ const sentence = failureSentence(message);
108299
+ return NETWORK_WORDS.some((w) => w.test(sentence));
108300
+ }
108301
+ function interruptionAfter(e, now, endedTurn) {
108302
+ switch (e.type) {
108303
+ case "error":
108304
+ if (!e.sessionId || !endedTurn || e.error.code === "adapter_crashed") return void 0;
108305
+ return isNetworkFailure(e.error.message) ? { reason: "network", at: now, detail: failureSentence(e.error.message).slice(0, 600) } : null;
108306
+ case "turn_complete":
108307
+ return null;
108308
+ case "state_change":
108309
+ if (e.state === "working") return null;
108310
+ if (e.state === "waiting_input" && e.reason === INTERRUPTED_BY_TOOL) return { reason: "interrupted", at: now };
108311
+ if (e.state === "waiting_input" && e.reason === "interrupted") return null;
108312
+ return void 0;
108313
+ default:
108314
+ return void 0;
108315
+ }
108316
+ }
108317
+ var RECONNECTING_FLAG_MS = 5 * 6e4;
108318
+ function interruptionWords(i) {
108319
+ switch (i.reason) {
108320
+ case "network":
108321
+ return "stopped: network error";
108322
+ case "quit":
108323
+ return "stopped: Centralu quit during its turn";
108324
+ case "shutdown":
108325
+ return `stopped: ${SHUTDOWN_WORDS} during its turn`;
108326
+ case "interrupted":
108327
+ return "stopped: its tool interrupted the turn";
108328
+ case "reconnecting":
108329
+ return `reconnecting to its API for ${Math.max(1, Math.round((Date.now() - i.at) / 6e4))} min`;
108330
+ }
108331
+ }
108332
+ var wentWithItsProcess = (i) => i?.reason === "quit" || i?.reason === "shutdown";
108333
+ var UPDATE_CONTINUE_LINE = "Centralu was updated during this turn, which stopped it; Centralu continued it on the new version.";
108334
+ var UPDATE_TASKS_LINE = "Centralu was updated, which stopped background work of this session; Centralu told the agent so it can start again what it needs.";
108335
+
108336
+ // packages/agent-host/src/sessions/tool-answers.ts
108337
+ function trustedJsonText(value) {
108338
+ return JSON.stringify(value).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
108339
+ }
108340
+ function plainQuoted(value, max = 60) {
108341
+ const cut3 = value.length > max ? `${value.slice(0, max)}\u2026` : value;
108342
+ return JSON.stringify(cut3).replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, (c) => {
108343
+ const code2 = c.codePointAt(0);
108344
+ return code2 > 65535 ? `\\u{${code2.toString(16)}}` : `\\u${code2.toString(16).padStart(4, "0")}`;
108345
+ });
108346
+ }
108347
+ var READER_LIST_CAP = 40;
108348
+ async function listSessionsText(tools, caller, project2) {
108349
+ const all = await tools.listSessions(project2 ? { project: project2 } : void 0);
108350
+ if (all.length === 0) {
108351
+ if (project2) return { text: `No session in a project called ${frameField(project2)}. Leave project out to list every one.` };
108352
+ return { text: caller.profile === "reader" ? "There are no other sessions." : "There are no sessions under management." };
108353
+ }
108354
+ const list2 = caller.profile === "reader" ? all.slice(0, READER_LIST_CAP) : all;
108355
+ const more = all.length - list2.length;
108356
+ return {
108357
+ text: list2.map(
108358
+ (s) => (
108359
+ // A linked machine's session says where it runs (#82 phase 4), and why the row is only what was last known
108360
+ // Names on one line (#838): a session's name is set by rename or an auto-name, and a line break in it made a row of its own
108361
+ `- ${frameField(s.name)} [${s.sessionId}] \xB7 project ${frameField(s.project)}${s.machine ? ` on ${frameField(s.machine)}` : ""} \xB7 ${s.tool} \xB7 ${s.waitingOn === "question" ? "waiting on a question" : s.state}` + (s.away ? ` \xB7 ${s.away}` : "") + // A turn cut short by the network or a quit looks like any session at rest (#698)
108362
+ (s.interruption ? ` \xB7 ${interruptionWords(s.interruption)}` : "") + // If merge status is not shown, the manager keeps assigning work to a finished branch (#69, dogfooding)
108363
+ (s.merged ? " \xB7 merged" : "") + // PR status (#76 stage 3) — assigning new work to a branch awaiting review pollutes the PR
108364
+ (s.pr ? ` \xB7 PR #${s.pr.number}(${s.pr.state})` : "") + (s.lastActive ? ` \xB7 last ${s.lastActive}` : "") + (s.preview ? `
108365
+ recent (JSON): ${trustedJsonText(s.preview)}` : "")
108366
+ )
108367
+ ).join("\n") + (more > 0 ? `
108368
+ ${more} more; pass project to narrow.` : "")
108369
+ };
108370
+ }
108371
+ function cardsText(cards) {
108372
+ return cards.map(
108373
+ (c) => c.kind === "question" ? `card ${c.card} (question), as JSON: ${trustedJsonText(JSON.stringify(c.questions.map((q2) => ({ question: q2.question, header: q2.header, options: q2.options.map((o) => o.label), ...q2.options.some((o) => o.description) ? { descriptions: q2.options.map((o) => o.description) } : {}, multiSelect: q2.multiSelect }))))}` : `card ${c.card} (approval${c.answerable ? ", yours to allow or deny" : ", only the person can answer it"}), as JSON: ${trustedJsonText(JSON.stringify(c.detail))}`
108374
+ ).join("\n");
108375
+ }
108376
+ function progressText(p) {
108377
+ if (!p) return "";
108378
+ return ` It last recorded something ${p.quietFor} ago${p.lastStep ? `; its last step, as JSON: ${JSON.stringify(p.lastStep)}` : ""}.`;
108379
+ }
108380
+ function busyText(b2, call2) {
108381
+ const what = {
108382
+ note: "add it to the running turn as a note; that turn's answer stays its owner's, and you read the reply to your note",
108383
+ queue: "hold it and deliver it as a new turn when the running one ends; you are told when that turn ends",
108384
+ fork: "start a sub-session from a copy of its conversation now, linked to you, in a worktree of its own (fork_read: read-only work in its folder)"
108385
+ };
108386
+ const lines = b2.choices.map((c) => `- ${c.choice}: ${c.available ? `${what[c.choice]}. ${call2(c.choice === "fork" ? "fork" : c.choice)}` : `not available: ${c.why}`}`);
108387
+ const ahead = b2.ahead ? ` ${b2.ahead} request${b2.ahead === 1 ? " is" : "s are"} queued there already.` : "";
108388
+ return `"${b2.sessionName}" [${b2.sessionId}] is busy (${b2.sessionState}) with a turn that is not yours, so nothing was sent.${progressText(b2.progress)}${ahead} Choose:
108389
+ ${lines.join("\n")}`;
108390
+ }
108391
+ function queuedText(q2) {
108392
+ const instead = q2.lowMemory ? `Not forked: memory is low (${q2.lowMemory}), so it was queued instead, which starts no new process. ` : "";
108393
+ return `${instead}Queued for "${q2.sessionName}" [${q2.sessionId}] as ${q2.queueId}, number ${q2.position} in its queue: it goes in as a new turn when the running one ends, and you are told when that turn ends.`;
108394
+ }
108395
+ var ANSWER_FORM = 'Answer a question with its option label, or your own words; one line per question; several choices of one question separated by ";". The person may answer first; the first answer counts.';
108396
+ function askProjectText(r) {
108397
+ if (!r.ok) return { text: r.error, isError: true };
108398
+ const where2 = `the session "${r.sessionName}" [${r.sessionId}] in ${r.project}`;
108399
+ if (r.state === "busy") return { text: busyText(r, (c) => `ask_project with the same task and ifBusy "${c}".`) };
108400
+ if (r.state === "queued") return { text: `${queuedText(r)} Call ask_project with project "${r.project}", session "${r.sessionId}" and no task to see where it stands.` };
108401
+ if (r.state === "asking") {
108402
+ return {
108403
+ text: `${where2} is waiting on you in the task you gave it:
108404
+ ${cardsText(r.cards)}
108405
+ Answer with agent_session(sessionId="${r.sessionId}", action="answer", card=\u2026, text=\u2026). ${ANSWER_FORM} Then call ask_project with project "${r.project}", session "${r.sessionId}" and no task to wait for the answer.`
108406
+ };
108407
+ }
108408
+ if (r.state === "working") {
108409
+ return {
108410
+ text: `${r.project} is still working on it in ${where2}${r.notice ? ` \u2014 ${r.notice}` : ""}.${progressText(r.progress)} To keep waiting, call ask_project again with project "${r.project}", session "${r.sessionId}" and no task; the work goes on meanwhile.`
108411
+ };
108412
+ }
108413
+ const lines = [
108414
+ r.paused ? `${r.project} has not finished (from ${where2}): ${r.paused} What it said so far, as JSON:` : `${r.project} answered (from ${where2}), as JSON:`,
108415
+ trustedJsonText(r.answer || "(no final text)")
108416
+ ];
108417
+ if (r.readable.length) lines.push(`You can read these now: ${r.readable.join(", ")}`);
108418
+ if (r.outside.length) lines.push(`Named outside ${r.project}'s folder (not opened for you): ${r.outside.join(", ")}`);
108419
+ return { text: lines.join("\n") };
108420
+ }
108421
+ var NO_POLLING = "Do not check on it with status meanwhile. With nothing else to do, wait is the one call that blocks until the end:";
108422
+ var NEW_ROLE_NEW_AGENT = "For another kind of work (say, editing files after read-only research), start a new agent instead, with the task written in full: it starts without this one's context, and this one's name and role came from its first task.";
108423
+ function cleanUpLine(sessionId, call2 = `agent_session(sessionId="${sessionId}", action="delete")`) {
108424
+ return `Archive it once its answer is used: ${call2}. Keep it only if more work on the same thing is coming soon, and archive it when that is done. Work not on the trunk is kept in the session archive.`;
108425
+ }
108426
+ function agentListText(r) {
108427
+ if (!r.ok) return { text: r.error, isError: true };
108428
+ const rows3 = r.agents;
108429
+ if (rows3.length === 0) return { text: "No session start_agent started from here is left." };
108430
+ const finished = rows3.filter((r5) => r5.state === "finished").length;
108431
+ const lines = rows3.map(
108432
+ (r5) => `- ${frameField(r5.name)} [${r5.sessionId}] \xB7 ${r5.tool} \xB7 ${r5.state}${r5.asleep ? " (asleep)" : ""}` + (r5.machine ? ` \xB7 on ${frameField(r5.machine)}` : "") + (r5.worktree ? ` \xB7 worktree branch ${frameField(r5.worktree.branch)}${r5.worktree.unmerged ? ", not merged: a delete keeps its work in the session archive" : ", merged"}` : "") + (r5.lastActive ? ` \xB7 last ${r5.lastActive}` : "")
108433
+ );
108434
+ return {
108435
+ text: `Sessions you started (${rows3.length}, ${finished} finished):
108436
+ ${lines.join("\n")}
108437
+ ` + (finished ? 'Delete each finished one whose answer you have used: agent_session(sessionId=\u2026, action="delete").' : "None is finished yet.")
108438
+ };
108439
+ }
108440
+ function agentRunText(r) {
108441
+ if (!r.ok) return { text: r.error, isError: true };
108442
+ if (r.state === "pending" && r.lowMemory) {
108443
+ return {
108444
+ text: `queued: low memory. No session yet: starting ${r.tool} is allowed and waits for memory (${r.lowMemory.available} free, ${r.lowMemory.floor} needed; request ${r.sessionId}). It starts when memory frees up, and you will be told when its turn ends; if memory stays low for ${r.lowMemory.maxWaitMinutes} minutes it gives up and you will be told. Carry on with other work, and do not do this with your own subagents instead. ${NO_POLLING} agent_session(sessionId="${r.sessionId}", action="wait") waits, action="stop" withdraws it.`
108445
+ };
108446
+ }
108447
+ if (r.state === "pending") {
108448
+ return {
108449
+ text: `No session yet: starting ${r.tool} is waiting for the person's answer on the card in your session (request ${r.sessionId}). Carry on with other work, and do not do this with your own subagents instead. If they allow it, the session starts and you will be told when its turn ends; if they refuse or it is withdrawn, you will be told. ${NO_POLLING} agent_session(sessionId="${r.sessionId}", action="wait") waits for the answer, action="stop" withdraws the request.`
108450
+ };
108451
+ }
108452
+ const who = `the ${r.tool} session "${r.sessionName}" [${r.sessionId}]` + (r.via === "fork" ? `, a fork of [${r.forkedFrom}]` : "");
108453
+ const out = agentStateText(r, who);
108454
+ return r.via === "note" ? { ...out, text: `Added as a note to the running turn. ${out.text}` } : out;
108455
+ }
108456
+ function agentStateText(r, who) {
108457
+ switch (r.state) {
108458
+ case "working":
108459
+ return {
108460
+ text: `${who} is working on it${r.notice ? ` \u2014 ${r.notice}` : ""}.${progressText(r.progress)} ` + (r.reported ? `You will be told when it ends. ${NO_POLLING} ` : "") + `agent_session(sessionId="${r.sessionId}", action="wait") waits for its answer, action="stop" stops it.`
108461
+ };
108462
+ case "asking":
108463
+ return {
108464
+ text: `${who} is waiting on you in the turn you gave it:
108465
+ ${cardsText(r.cards)}
108466
+ agent_session(sessionId="${r.sessionId}", action="answer", card=\u2026, text=\u2026) answers it. ${ANSWER_FORM}`
108467
+ };
108468
+ case "busy":
108469
+ return { text: busyText(r, (c) => `agent_session(sessionId="${r.sessionId}", action="send", text=\u2026, ifBusy="${c}").`) };
108470
+ case "queued":
108471
+ return { text: `${queuedText(r)} agent_session(sessionId="${r.sessionId}", action="stop") withdraws it.` };
108472
+ case "answered":
108473
+ return { text: `Answered card ${r.card} in ${who}; it goes on. agent_session(sessionId="${r.sessionId}", action="wait") waits for its answer.` };
108474
+ case "stopped":
108475
+ if (!r.sessionName) return { text: `Withdrew request ${r.sessionId}: its card is gone and no session was started.` };
108476
+ return { text: `Stopped ${who}.${r.notice ? ` ${r.notice}` : ""}` };
108477
+ case "deleted":
108478
+ if (r.branch === void 0) return { text: `Moved ${who} to the session archive. Tell the person in one line.` };
108479
+ if (r.note) return { text: `Moved ${who} to the session archive. ${r.note} Tell the person in one line.` };
108480
+ return {
108481
+ text: (r.warning ? `Moved ${who} to the session archive. Warning: ${r.warning} ` : `Cleaned up ${who}: its worktree and branch "${r.branch}" were removed and the session was archived. `) + (r.setAside.changedFiles || r.setAside.commits ? `${r.setAside.changedFiles} uncommitted files, ${r.setAside.commits} commits not on the trunk, kept in the session archive. ` : "Nothing needed setting aside. ") + "Tell the person in one line."
108482
+ };
108483
+ case "at_rest":
108484
+ return {
108485
+ text: `${who} is not working on anything you gave it (${r.sessionState}${r.interruption ? `, ${interruptionWords(r.interruption)}` : ""}). Its last answer, as JSON:
108486
+ ${trustedJsonText(r.answer || "(no final text)")}
108487
+ ` + (r.notice ? `${r.notice}
108488
+ ` : "") + `agent_session(sessionId="${r.sessionId}", action="send", text=\u2026) gives it more. ${NEW_ROLE_NEW_AGENT}` + (r.cleanUp ? `
108489
+ ${cleanUpLine(r.sessionId)}` : "")
108490
+ };
108491
+ case "done": {
108492
+ const lines = [`${who} answered, as JSON:`, trustedJsonText(r.answer || "(no final text)")];
108493
+ if (r.notice) lines.push(r.notice);
108494
+ if (r.readable.length) lines.push(`You can read these now: ${r.readable.join(", ")}`);
108495
+ if (r.outside.length) lines.push(`Named outside its folder (not opened for you): ${r.outside.join(", ")}`);
108496
+ lines.push(`agent_session(sessionId="${r.sessionId}", action="send", text=\u2026) continues with it. ${NEW_ROLE_NEW_AGENT}`);
108497
+ if (r.cleanUp) lines.push(cleanUpLine(r.sessionId));
108498
+ return { text: lines.join("\n") };
108499
+ }
108500
+ }
108501
+ }
108502
+ var plural = (n, one2, many = `${one2}s`) => `${n} ${n === 1 ? one2 : many}`;
108503
+
108504
+ // packages/agent-host/src/sessions/stale-session-id.ts
108505
+ function staleSessionHint(host, id3, visible, seesAll = false, doing = "act") {
108506
+ if (host.meta.has(id3)) return "";
108507
+ const live2 = [...host.meta.values()].filter((s) => s.kind !== "orchestrator" && visible(s));
108508
+ const found = host.store.trash.trashedSession(id3);
108509
+ const trashed = found && (seesAll || live2.some((s) => s.projectId === found.record.projectId)) ? found : null;
108510
+ const heirId = liveHeirOf(host.store, id3, (h) => {
108511
+ const s = host.meta.get(h);
108512
+ return !!s && s.kind !== "orchestrator" && visible(s);
108513
+ });
108514
+ const heir = heirId ? host.meta.get(heirId) : void 0;
108515
+ if (!trashed && !heir) return "";
108516
+ const named = trashed ? live2.filter((s) => s.name === trashed.session.name && s.projectId === trashed.record.projectId && s !== heir) : [];
108517
+ const label = (s) => `${plainQuoted(s.name, 80)} (id ${s.id})`;
108518
+ const parts = [];
108519
+ if (trashed) parts.push(`That id was the session ${plainQuoted(trashed.session.name, 80)}, deleted since (it is in the archive).`);
108520
+ else parts.push("That id was a session that has been replaced by a handoff.");
108521
+ if (heir) parts.push(`The live session that took over from it: ${label(heir)}.`);
108522
+ if (named.length) parts.push(`Live in the same project with the same name: ${named.map(label).join(", ")}.`);
108523
+ const then2 = doing === "send" ? "Nothing was sent; send to the one you meant, by its id." : doing === "read" ? "Nothing was read; read the one you meant, by its id." : "Nothing was done; retry the same call with the id of the live session you meant.";
108524
+ parts.push(heir || named.length ? then2 : "No live session carries its name; list the sessions to find the one you meant.");
108525
+ return " " + parts.join(" ");
108526
+ }
108527
+
108241
108528
  // packages/agent-host/src/mail/script-label.ts
108242
108529
  var LABEL_MAX = 40;
108243
108530
  var NOT_IN_A_LABEL = /[^\p{L}\p{N} ._:@/+-]/gu;
@@ -108533,8 +108820,9 @@ function orchestratorToolsFor(host, orchestratorId, scopeIds, ownProject, hubVie
108533
108820
  const trusted = trustedIds();
108534
108821
  return (id3) => !trusted.has(id3);
108535
108822
  };
108536
- const readError = (id3) => ownProject && wide() ? `Not a session you can read: ${id3}` : scopeError(id3);
108537
- const scopeError = (id3) => scopeIds !== void 0 ? `Not a member of this coordinating session: ${id3}` : ownProject ? `Not a session in this project: ${id3}` : hubView ? `Not a session this machine shares: ${id3}` : `Not a session this app manages: ${id3}`;
108823
+ const readError = (id3) => (ownProject && wide() ? `Not a session you can read: ${id3}` : scopeText(id3)) + staleSessionHint(host, id3, readable, central, "read");
108824
+ const scopeError = (id3, doing = "act") => scopeText(id3) + staleSessionHint(host, id3, inScope, central, doing);
108825
+ const scopeText = (id3) => scopeIds !== void 0 ? `Not a member of this coordinating session: ${id3}` : ownProject ? `Not a session in this project: ${id3}` : hubView ? `Not a session this machine shares: ${id3}` : `Not a session this app manages: ${id3}`;
108538
108826
  const sendLinked = async (reader, sessionId, text3, reportBack) => {
108539
108827
  const earlier = host.children.linkedReports.get(sessionId);
108540
108828
  const mine = reportBack ? { orchestratorId, busy: false, instruction: text3 } : void 0;
@@ -108855,7 +109143,7 @@ function orchestratorToolsFor(host, orchestratorId, scopeIds, ownProject, hubVie
108855
109143
  return sendLinked(remote, sessionId, text3, reportBack);
108856
109144
  }
108857
109145
  const target = host.meta.get(sessionId);
108858
- if (!target || !inScope(target)) return { ok: false, error: scopeError(sessionId) };
109146
+ if (!target || !inScope(target)) return { ok: false, error: scopeError(sessionId, "send") };
108859
109147
  if (reportBack) {
108860
109148
  const refused2 = host.refusesDelegation(orchestratorId, { sessionId, projectId: target.projectId }, { tool: "send_to_session", target: target.name || target.id, text: text3 });
108861
109149
  if (refused2) return { ok: false, error: refused2 };
@@ -109291,55 +109579,6 @@ function withHubMark(settings, info) {
109291
109579
  // packages/agent-host/src/sessions/queued-requests.ts
109292
109580
  import { randomUUID as randomUUID11 } from "node:crypto";
109293
109581
 
109294
- // packages/agent-host/src/sessions/interruptions.ts
109295
- var NETWORK_WORDS = [
109296
- /\b(ECONNREFUSED|ENOTFOUND|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|EHOSTUNREACH|ENETDOWN)\b/,
109297
- /API Error: (No response from API|Can't reach the API server|Connection (refused|lost|reset|error|dropped)|Request timed out|No internet route|fetch failed|Unable to connect to API(?!: SSL))/i,
109298
- /computer (went to sleep|was asleep)/i,
109299
- /stream disconnected before completion/i,
109300
- /failed to lookup address information/i,
109301
- /Connection failed: error sending request/i,
109302
- /waiting for network/i
109303
- ];
109304
- function failureSentence(message) {
109305
- return message.split(/(?:^|[.\s])stderr:/)[0].trim();
109306
- }
109307
- function isNetworkFailure(message) {
109308
- const sentence = failureSentence(message);
109309
- return NETWORK_WORDS.some((w) => w.test(sentence));
109310
- }
109311
- function interruptionAfter(e, now, endedTurn) {
109312
- switch (e.type) {
109313
- case "error":
109314
- if (!e.sessionId || !endedTurn || e.error.code === "adapter_crashed") return void 0;
109315
- return isNetworkFailure(e.error.message) ? { reason: "network", at: now, detail: failureSentence(e.error.message).slice(0, 600) } : null;
109316
- case "turn_complete":
109317
- return null;
109318
- case "state_change":
109319
- if (e.state === "working") return null;
109320
- if (e.state === "waiting_input" && e.reason === INTERRUPTED_BY_TOOL) return { reason: "interrupted", at: now };
109321
- if (e.state === "waiting_input" && e.reason === "interrupted") return null;
109322
- return void 0;
109323
- default:
109324
- return void 0;
109325
- }
109326
- }
109327
- var RECONNECTING_FLAG_MS = 5 * 6e4;
109328
- function interruptionWords(i) {
109329
- switch (i.reason) {
109330
- case "network":
109331
- return "stopped: network error";
109332
- case "quit":
109333
- return "stopped: Centralu quit during its turn";
109334
- case "interrupted":
109335
- return "stopped: its tool interrupted the turn";
109336
- case "reconnecting":
109337
- return `reconnecting to its API for ${Math.max(1, Math.round((Date.now() - i.at) / 6e4))} min`;
109338
- }
109339
- }
109340
- var UPDATE_CONTINUE_LINE = "Centralu was updated during this turn, which stopped it; Centralu continued it on the new version.";
109341
- var UPDATE_TASKS_LINE = "Centralu was updated, which stopped background work of this session; Centralu told the agent so it can start again what it needs.";
109342
-
109343
109582
  // packages/agent-host/src/sessions/error-kind.ts
109344
109583
  var USAGE_LIMIT = /usage limit|rate.?limit|\b429\b|quota|limit reached|hit your limit/i;
109345
109584
  var OVERLOADED = /overload|\b529\b|\b503\b|capacity|temporarily unavailable/i;
@@ -109610,174 +109849,6 @@ ${reason}`
109610
109849
  // packages/agent-host/src/sessions/delegated-cards.ts
109611
109850
  import { createHash as createHash9 } from "node:crypto";
109612
109851
 
109613
- // packages/agent-host/src/sessions/tool-answers.ts
109614
- function trustedJsonText(value) {
109615
- return JSON.stringify(value).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
109616
- }
109617
- function plainQuoted(value, max = 60) {
109618
- const cut3 = value.length > max ? `${value.slice(0, max)}\u2026` : value;
109619
- return JSON.stringify(cut3).replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, (c) => {
109620
- const code2 = c.codePointAt(0);
109621
- return code2 > 65535 ? `\\u{${code2.toString(16)}}` : `\\u${code2.toString(16).padStart(4, "0")}`;
109622
- });
109623
- }
109624
- var READER_LIST_CAP = 40;
109625
- async function listSessionsText(tools, caller, project2) {
109626
- const all = await tools.listSessions(project2 ? { project: project2 } : void 0);
109627
- if (all.length === 0) {
109628
- if (project2) return { text: `No session in a project called ${frameField(project2)}. Leave project out to list every one.` };
109629
- return { text: caller.profile === "reader" ? "There are no other sessions." : "There are no sessions under management." };
109630
- }
109631
- const list2 = caller.profile === "reader" ? all.slice(0, READER_LIST_CAP) : all;
109632
- const more = all.length - list2.length;
109633
- return {
109634
- text: list2.map(
109635
- (s) => (
109636
- // A linked machine's session says where it runs (#82 phase 4), and why the row is only what was last known
109637
- // Names on one line (#838): a session's name is set by rename or an auto-name, and a line break in it made a row of its own
109638
- `- ${frameField(s.name)} [${s.sessionId}] \xB7 project ${frameField(s.project)}${s.machine ? ` on ${frameField(s.machine)}` : ""} \xB7 ${s.tool} \xB7 ${s.waitingOn === "question" ? "waiting on a question" : s.state}` + (s.away ? ` \xB7 ${s.away}` : "") + // A turn cut short by the network or a quit looks like any session at rest (#698)
109639
- (s.interruption ? ` \xB7 ${interruptionWords(s.interruption)}` : "") + // If merge status is not shown, the manager keeps assigning work to a finished branch (#69, dogfooding)
109640
- (s.merged ? " \xB7 merged" : "") + // PR status (#76 stage 3) — assigning new work to a branch awaiting review pollutes the PR
109641
- (s.pr ? ` \xB7 PR #${s.pr.number}(${s.pr.state})` : "") + (s.lastActive ? ` \xB7 last ${s.lastActive}` : "") + (s.preview ? `
109642
- recent (JSON): ${trustedJsonText(s.preview)}` : "")
109643
- )
109644
- ).join("\n") + (more > 0 ? `
109645
- ${more} more; pass project to narrow.` : "")
109646
- };
109647
- }
109648
- function cardsText(cards) {
109649
- return cards.map(
109650
- (c) => c.kind === "question" ? `card ${c.card} (question), as JSON: ${trustedJsonText(JSON.stringify(c.questions.map((q2) => ({ question: q2.question, header: q2.header, options: q2.options.map((o) => o.label), ...q2.options.some((o) => o.description) ? { descriptions: q2.options.map((o) => o.description) } : {}, multiSelect: q2.multiSelect }))))}` : `card ${c.card} (approval${c.answerable ? ", yours to allow or deny" : ", only the person can answer it"}), as JSON: ${trustedJsonText(JSON.stringify(c.detail))}`
109651
- ).join("\n");
109652
- }
109653
- function progressText(p) {
109654
- if (!p) return "";
109655
- return ` It last recorded something ${p.quietFor} ago${p.lastStep ? `; its last step, as JSON: ${JSON.stringify(p.lastStep)}` : ""}.`;
109656
- }
109657
- function busyText(b2, call2) {
109658
- const what = {
109659
- note: "add it to the running turn as a note; that turn's answer stays its owner's, and you read the reply to your note",
109660
- queue: "hold it and deliver it as a new turn when the running one ends; you are told when that turn ends",
109661
- fork: "start a sub-session from a copy of its conversation now, linked to you, in a worktree of its own (fork_read: read-only work in its folder)"
109662
- };
109663
- const lines = b2.choices.map((c) => `- ${c.choice}: ${c.available ? `${what[c.choice]}. ${call2(c.choice === "fork" ? "fork" : c.choice)}` : `not available: ${c.why}`}`);
109664
- const ahead = b2.ahead ? ` ${b2.ahead} request${b2.ahead === 1 ? " is" : "s are"} queued there already.` : "";
109665
- return `"${b2.sessionName}" [${b2.sessionId}] is busy (${b2.sessionState}) with a turn that is not yours, so nothing was sent.${progressText(b2.progress)}${ahead} Choose:
109666
- ${lines.join("\n")}`;
109667
- }
109668
- function queuedText(q2) {
109669
- const instead = q2.lowMemory ? `Not forked: memory is low (${q2.lowMemory}), so it was queued instead, which starts no new process. ` : "";
109670
- return `${instead}Queued for "${q2.sessionName}" [${q2.sessionId}] as ${q2.queueId}, number ${q2.position} in its queue: it goes in as a new turn when the running one ends, and you are told when that turn ends.`;
109671
- }
109672
- var ANSWER_FORM = 'Answer a question with its option label, or your own words; one line per question; several choices of one question separated by ";". The person may answer first; the first answer counts.';
109673
- function askProjectText(r) {
109674
- if (!r.ok) return { text: r.error, isError: true };
109675
- const where2 = `the session "${r.sessionName}" [${r.sessionId}] in ${r.project}`;
109676
- if (r.state === "busy") return { text: busyText(r, (c) => `ask_project with the same task and ifBusy "${c}".`) };
109677
- if (r.state === "queued") return { text: `${queuedText(r)} Call ask_project with project "${r.project}", session "${r.sessionId}" and no task to see where it stands.` };
109678
- if (r.state === "asking") {
109679
- return {
109680
- text: `${where2} is waiting on you in the task you gave it:
109681
- ${cardsText(r.cards)}
109682
- Answer with agent_session(sessionId="${r.sessionId}", action="answer", card=\u2026, text=\u2026). ${ANSWER_FORM} Then call ask_project with project "${r.project}", session "${r.sessionId}" and no task to wait for the answer.`
109683
- };
109684
- }
109685
- if (r.state === "working") {
109686
- return {
109687
- text: `${r.project} is still working on it in ${where2}${r.notice ? ` \u2014 ${r.notice}` : ""}.${progressText(r.progress)} To keep waiting, call ask_project again with project "${r.project}", session "${r.sessionId}" and no task; the work goes on meanwhile.`
109688
- };
109689
- }
109690
- const lines = [
109691
- r.paused ? `${r.project} has not finished (from ${where2}): ${r.paused} What it said so far, as JSON:` : `${r.project} answered (from ${where2}), as JSON:`,
109692
- trustedJsonText(r.answer || "(no final text)")
109693
- ];
109694
- if (r.readable.length) lines.push(`You can read these now: ${r.readable.join(", ")}`);
109695
- if (r.outside.length) lines.push(`Named outside ${r.project}'s folder (not opened for you): ${r.outside.join(", ")}`);
109696
- return { text: lines.join("\n") };
109697
- }
109698
- var NO_POLLING = "Do not check on it with status meanwhile. With nothing else to do, wait is the one call that blocks until the end:";
109699
- var NEW_ROLE_NEW_AGENT = "For another kind of work (say, editing files after read-only research), start a new agent instead, with the task written in full: it starts without this one's context, and this one's name and role came from its first task.";
109700
- function cleanUpLine(sessionId, call2 = `agent_session(sessionId="${sessionId}", action="delete")`) {
109701
- return `Archive it once its answer is used: ${call2}. Keep it only if more work on the same thing is coming soon, and archive it when that is done. Work not on the trunk is kept in the session archive.`;
109702
- }
109703
- function agentListText(r) {
109704
- if (!r.ok) return { text: r.error, isError: true };
109705
- const rows3 = r.agents;
109706
- if (rows3.length === 0) return { text: "No session start_agent started from here is left." };
109707
- const finished = rows3.filter((r5) => r5.state === "finished").length;
109708
- const lines = rows3.map(
109709
- (r5) => `- ${frameField(r5.name)} [${r5.sessionId}] \xB7 ${r5.tool} \xB7 ${r5.state}${r5.asleep ? " (asleep)" : ""}` + (r5.machine ? ` \xB7 on ${frameField(r5.machine)}` : "") + (r5.worktree ? ` \xB7 worktree branch ${frameField(r5.worktree.branch)}${r5.worktree.unmerged ? ", not merged: a delete keeps its work in the session archive" : ", merged"}` : "") + (r5.lastActive ? ` \xB7 last ${r5.lastActive}` : "")
109710
- );
109711
- return {
109712
- text: `Sessions you started (${rows3.length}, ${finished} finished):
109713
- ${lines.join("\n")}
109714
- ` + (finished ? 'Delete each finished one whose answer you have used: agent_session(sessionId=\u2026, action="delete").' : "None is finished yet.")
109715
- };
109716
- }
109717
- function agentRunText(r) {
109718
- if (!r.ok) return { text: r.error, isError: true };
109719
- if (r.state === "pending" && r.lowMemory) {
109720
- return {
109721
- text: `queued: low memory. No session yet: starting ${r.tool} is allowed and waits for memory (${r.lowMemory.available} free, ${r.lowMemory.floor} needed; request ${r.sessionId}). It starts when memory frees up, and you will be told when its turn ends; if memory stays low for ${r.lowMemory.maxWaitMinutes} minutes it gives up and you will be told. Carry on with other work, and do not do this with your own subagents instead. ${NO_POLLING} agent_session(sessionId="${r.sessionId}", action="wait") waits, action="stop" withdraws it.`
109722
- };
109723
- }
109724
- if (r.state === "pending") {
109725
- return {
109726
- text: `No session yet: starting ${r.tool} is waiting for the person's answer on the card in your session (request ${r.sessionId}). Carry on with other work, and do not do this with your own subagents instead. If they allow it, the session starts and you will be told when its turn ends; if they refuse or it is withdrawn, you will be told. ${NO_POLLING} agent_session(sessionId="${r.sessionId}", action="wait") waits for the answer, action="stop" withdraws the request.`
109727
- };
109728
- }
109729
- const who = `the ${r.tool} session "${r.sessionName}" [${r.sessionId}]` + (r.via === "fork" ? `, a fork of [${r.forkedFrom}]` : "");
109730
- const out = agentStateText(r, who);
109731
- return r.via === "note" ? { ...out, text: `Added as a note to the running turn. ${out.text}` } : out;
109732
- }
109733
- function agentStateText(r, who) {
109734
- switch (r.state) {
109735
- case "working":
109736
- return {
109737
- text: `${who} is working on it${r.notice ? ` \u2014 ${r.notice}` : ""}.${progressText(r.progress)} ` + (r.reported ? `You will be told when it ends. ${NO_POLLING} ` : "") + `agent_session(sessionId="${r.sessionId}", action="wait") waits for its answer, action="stop" stops it.`
109738
- };
109739
- case "asking":
109740
- return {
109741
- text: `${who} is waiting on you in the turn you gave it:
109742
- ${cardsText(r.cards)}
109743
- agent_session(sessionId="${r.sessionId}", action="answer", card=\u2026, text=\u2026) answers it. ${ANSWER_FORM}`
109744
- };
109745
- case "busy":
109746
- return { text: busyText(r, (c) => `agent_session(sessionId="${r.sessionId}", action="send", text=\u2026, ifBusy="${c}").`) };
109747
- case "queued":
109748
- return { text: `${queuedText(r)} agent_session(sessionId="${r.sessionId}", action="stop") withdraws it.` };
109749
- case "answered":
109750
- return { text: `Answered card ${r.card} in ${who}; it goes on. agent_session(sessionId="${r.sessionId}", action="wait") waits for its answer.` };
109751
- case "stopped":
109752
- if (!r.sessionName) return { text: `Withdrew request ${r.sessionId}: its card is gone and no session was started.` };
109753
- return { text: `Stopped ${who}.${r.notice ? ` ${r.notice}` : ""}` };
109754
- case "deleted":
109755
- if (r.branch === void 0) return { text: `Moved ${who} to the session archive. Tell the person in one line.` };
109756
- if (r.note) return { text: `Moved ${who} to the session archive. ${r.note} Tell the person in one line.` };
109757
- return {
109758
- text: (r.warning ? `Moved ${who} to the session archive. Warning: ${r.warning} ` : `Cleaned up ${who}: its worktree and branch "${r.branch}" were removed and the session was archived. `) + (r.setAside.changedFiles || r.setAside.commits ? `${r.setAside.changedFiles} uncommitted files, ${r.setAside.commits} commits not on the trunk, kept in the session archive. ` : "Nothing needed setting aside. ") + "Tell the person in one line."
109759
- };
109760
- case "at_rest":
109761
- return {
109762
- text: `${who} is not working on anything you gave it (${r.sessionState}${r.interruption ? `, ${interruptionWords(r.interruption)}` : ""}). Its last answer, as JSON:
109763
- ${trustedJsonText(r.answer || "(no final text)")}
109764
- ` + (r.notice ? `${r.notice}
109765
- ` : "") + `agent_session(sessionId="${r.sessionId}", action="send", text=\u2026) gives it more. ${NEW_ROLE_NEW_AGENT}` + (r.cleanUp ? `
109766
- ${cleanUpLine(r.sessionId)}` : "")
109767
- };
109768
- case "done": {
109769
- const lines = [`${who} answered, as JSON:`, trustedJsonText(r.answer || "(no final text)")];
109770
- if (r.notice) lines.push(r.notice);
109771
- if (r.readable.length) lines.push(`You can read these now: ${r.readable.join(", ")}`);
109772
- if (r.outside.length) lines.push(`Named outside its folder (not opened for you): ${r.outside.join(", ")}`);
109773
- lines.push(`agent_session(sessionId="${r.sessionId}", action="send", text=\u2026) continues with it. ${NEW_ROLE_NEW_AGENT}`);
109774
- if (r.cleanUp) lines.push(cleanUpLine(r.sessionId));
109775
- return { text: lines.join("\n") };
109776
- }
109777
- }
109778
- }
109779
- var plural = (n, one2, many = `${one2}s`) => `${n} ${n === 1 ? one2 : many}`;
109780
-
109781
109852
  // packages/agent-host/src/sessions/app-guide.ts
109782
109853
  var APP_GUIDE_TOPICS = ["overview", "sessions", "orchestrator", "apps", "approvals", "settings", "updates"];
109783
109854
  var STATIC = {
@@ -110129,8 +110200,8 @@ var ORCHESTRATOR_TOOLS = [
110129
110200
  */
110130
110201
  {
110131
110202
  name: "get_preferences",
110132
- description: "Reads the person's settings: theme and the themes to choose from, text size, fonts, line height, the notification switches, idle sleep, automatic updates.",
110133
- schema: external_exports.object({})
110203
+ description: "Reads the person's settings: theme and the themes to choose from (theme files with their problems), text size, fonts, line height, the notification switches, idle sleep, automatic updates, the models for started agents.",
110204
+ schema: external_exports.object({ themeFile: external_exports.string().optional().describe("A theme file id: shows that file in full (its tokens and problems), to edit it with set_preferences themeFile.") }).strict()
110134
110205
  },
110135
110206
  {
110136
110207
  name: "set_preferences",
@@ -110149,6 +110220,21 @@ var ORCHESTRATOR_TOOLS = [
110149
110220
  fold: external_exports.array(external_exports.string()).max(50).optional().describe("Sessions whose children to fold away in the sidebar."),
110150
110221
  unfold: external_exports.array(external_exports.string()).max(50).optional().describe("Sessions whose children to show again.")
110151
110222
  }).strict().optional().describe("Which sidebar groups of children are folded; the rest stay as they are."),
110223
+ agentModels: external_exports.record(
110224
+ external_exports.string(),
110225
+ external_exports.object({
110226
+ light: external_exports.object({ model: external_exports.string().min(1), effort: external_exports.string().optional() }).nullable().optional(),
110227
+ standard: external_exports.object({ model: external_exports.string().min(1), effort: external_exports.string().optional() }).nullable().optional(),
110228
+ heavy: external_exports.object({ model: external_exports.string().min(1), effort: external_exports.string().optional() }).nullable().optional()
110229
+ }).strict()
110230
+ ).optional().describe("The model (and effort) start_agent runs when a caller names a difficulty, per tool then light, standard or heavy; null clears one. The rest stay."),
110231
+ themeFile: external_exports.object({
110232
+ id: external_exports.string().optional().describe("An existing theme file to replace; leave out to make a new file named after the theme."),
110233
+ name: external_exports.string().describe("The theme name, 1 to 80 characters."),
110234
+ base: external_exports.enum(["dark", "light"]),
110235
+ tokens: external_exports.record(external_exports.string(), external_exports.string()).describe("Token key to CSS value (get_preferences themeFile shows the keys); a token left out keeps its preset value.")
110236
+ }).strict().optional().describe("Writes a theme file as Settings does; pick it afterwards with theme (its id or name)."),
110237
+ deleteThemeFile: external_exports.string().optional().describe("The id of a theme file to remove (kept in the themes folder under .removed); a side that showed it goes back to its preset."),
110152
110238
  notifications: external_exports.object({
110153
110239
  approval: external_exports.boolean().optional().describe("Notify when an agent is blocked on an approval."),
110154
110240
  done: external_exports.boolean().optional().describe("Notify when a session finishes out of sight."),
@@ -111314,6 +111400,310 @@ function movedMessage(m, folded = false) {
111314
111400
  return { handle: m.messageId, name: m.firstLine, state, can, message: true, ...folded ? { folded: true } : {} };
111315
111401
  }
111316
111402
 
111403
+ // packages/agent-host/src/mail/rules.ts
111404
+ var REFUSALS = [
111405
+ "bad_address",
111406
+ "not_local",
111407
+ "not_found",
111408
+ "not_participant",
111409
+ "thread_done",
111410
+ "kind_not_allowed",
111411
+ "field_not_allowed",
111412
+ "not_allowed",
111413
+ "empty",
111414
+ "too_long",
111415
+ "too_many_blockers",
111416
+ "blockers_stand",
111417
+ "no_blocker",
111418
+ "bad_follows",
111419
+ "no_mailbox",
111420
+ "rate_limited",
111421
+ "bad_value",
111422
+ "policy",
111423
+ // mail without threads (docs/plans/mailbox.md §15, S9)
111424
+ "gone",
111425
+ "ambiguous",
111426
+ "unknown",
111427
+ "elsewhere",
111428
+ "unknown_message",
111429
+ "own_message",
111430
+ "recipients",
111431
+ "not_yours",
111432
+ "pair_rate",
111433
+ "repeat",
111434
+ "ask_rate",
111435
+ "orchestrator_asks",
111436
+ "removed",
111437
+ "untrusted_project"
111438
+ ];
111439
+ function refuse4(reason, message, more = {}) {
111440
+ return rpcError(message, "refused", { retryable: false, data: { reason, ...more } });
111441
+ }
111442
+ function refusalReason(err) {
111443
+ const reason = asObj(asObj(err).data).reason;
111444
+ return REFUSALS.find((r) => r === reason);
111445
+ }
111446
+ function bodyLimit(sender) {
111447
+ return sender === "app" ? 4e3 : 16e3;
111448
+ }
111449
+ function postsPerMinute(sender) {
111450
+ return sender === "session" ? 30 : sender === "app" ? 10 : Infinity;
111451
+ }
111452
+ var ASKS_PER_HOUR = 5;
111453
+ var REPEAT_WINDOW_MS = 10 * 6e4;
111454
+ function wakes(sender, m) {
111455
+ if (m.kind === "event") return false;
111456
+ if (sender === "centralu" && m.wake === "no") return false;
111457
+ if (sender !== "app") return true;
111458
+ if (m.wake === "no") return false;
111459
+ if (m.kind === "notice") return m.needsOwner || m.wake === "yes";
111460
+ return true;
111461
+ }
111462
+ function needsReceiver(m) {
111463
+ return m.sender === PERSON || m.kind === "task" || m.kind === "report" || m.needsReply || m.facts.needsOwner === true;
111464
+ }
111465
+ var HOLD_QUESTION_MS = 5 * 6e4;
111466
+ var HOLD_OTHER_MS = 15 * 6e4;
111467
+ var DEBOUNCE_MS = 2e3;
111468
+ function dueAt(now, urgent, fromPerson) {
111469
+ return urgent || fromPerson ? now : now + DEBOUNCE_MS;
111470
+ }
111471
+ var MINUTE_MS = 6e4;
111472
+ var HOUR_MS = 60 * MINUTE_MS;
111473
+ var READ_ITEMS = 10;
111474
+ var READ_CHARS = 2e4;
111475
+ var READ_CHAIN = 9;
111476
+ var PAIR_NEW_PER_HOUR = 5;
111477
+ var MAX_RECIPIENTS = 10;
111478
+ var HOLD_UNTRUSTED_OUT = true;
111479
+ var MAX_DEPTH = 4;
111480
+ var HOST_MAIL_TURNS_PER_HOUR = 120;
111481
+ var HOST_SHARE = 10;
111482
+ var PAIR_TURNS_PER_HOUR = 20;
111483
+ var REMINDER = {
111484
+ /** After a session came to rest, before the first reminder */
111485
+ graceMs: 2 * 6e4,
111486
+ /** After a turn the person started */
111487
+ personQuietMs: 10 * 6e4,
111488
+ /** After the first reminder's turn ended; also the wait after the second before the row is released */
111489
+ againMs: 20 * 6e4,
111490
+ perRow: 2,
111491
+ /** Between two follow-up wakes of one session (reminders and notices to a sender together) */
111492
+ gapMs: 20 * 6e4,
111493
+ perDay: 6,
111494
+ /** A session asleep longer than this since its last turn is not woken for a follow-up (its prompt cache has expired) */
111495
+ warmMs: 50 * 6e4,
111496
+ /** After delivery a needsReply row is released: the one 2-hour constant, which also bounds a cover (D23, D25) */
111497
+ unansweredAfterMs: 2 * HOUR_MS
111498
+ };
111499
+ var REFUSAL_TEXT = {
111500
+ gone: (address) => `${address} is gone: it is in the session archive or deleted.`,
111501
+ no_mailbox: () => "that session cannot read mail: only an ordinary session in a project, or the orchestrator, has the mail tool",
111502
+ elsewhere: (address) => `${address} is on another computer, and mail does not reach other computers yet; use agent_session there.`,
111503
+ unknown_message: (id3) => `there is no message ${id3} you can read; check the id with mail(action="inbox").`,
111504
+ own_message: (id3) => `${id3} is your own message; say who it is for with to="\u2026".`,
111505
+ recipients: () => `at most ${MAX_RECIPIENTS} recipients.`,
111506
+ not_allowed: () => "that is not a party that takes mail",
111507
+ withheld: (id3) => `${id3} is a request: its words are shown in the turn it starts, and it is marked done there.`,
111508
+ not_yours: (id3) => `you were not sent ${id3} and did not send it.`,
111509
+ pair_rate: (name) => `you started ${PAIR_NEW_PER_HOUR} conversations with ${name} in the last hour; answer one of its messages instead, or wait.`,
111510
+ repeat: (name) => `you sent the same text to ${name} less than ${REPEAT_WINDOW_MS / 6e4} minutes ago.`,
111511
+ ask_rate: () => `your project's sessions asked the orchestrator ${ASKS_PER_HOUR} times in the last hour; wait, or write to the person.`,
111512
+ orchestrator_asks: () => "the orchestrator runs on a tool that cannot ask the person for each step of a request yet, so a session cannot ask it: write to the person instead",
111513
+ untrusted_project: (project2, target) => `your project ${project2} is not trusted, so work you give outside it (to ${target}) waits for the person to trust ${project2}. Nothing was started. Centralu shows the person how, and tells you when ${project2} is trusted; give it again then.`
111514
+ };
111515
+ var ADOPTED_TASK = "Work given before this version of Centralu wrote it down as mail. The conversation of the session that does it shows what was asked.";
111516
+ var TASK_CUT = "\n[Centralu: cut here. The full task is in the session's conversation (read_session).]";
111517
+ function cutTask(text3, limit) {
111518
+ const task = text3.trim();
111519
+ return task.length > limit ? task.slice(0, limit - TASK_CUT.length) + TASK_CUT : task;
111520
+ }
111521
+ function firstLineOf(text3, max = Infinity) {
111522
+ return oneLineText(text3.split(/\r\n|[\n\r]/)[0] ?? "", max);
111523
+ }
111524
+ function wakesMessage(m) {
111525
+ return m.facts.quiet !== true;
111526
+ }
111527
+ function quietAnswer(m, target, receiver) {
111528
+ return m.kind === "message" && !m.needsReply && !m.urgent && !receiver.request && target.kind === "message" && !target.needsReply && parseAddress(m.sender)?.kind === "session" && m.sender !== receiver.parent && !receiver.owesTask;
111529
+ }
111530
+ function depthOf2(cause2, about, recipients) {
111531
+ if (!cause2 || cause2.sender === PERSON) return 0;
111532
+ const followUp = cause2.kind === "notice" && (cause2.facts.reminder !== void 0 || cause2.facts.unanswered !== void 0);
111533
+ if (cause2.kind !== "message" && !followUp) return 0;
111534
+ const base2 = followUp ? Math.max(0, ...about.map((m) => m.depth)) : cause2.depth;
111535
+ const back = !followUp ? [cause2.sender] : cause2.facts.reminder !== void 0 ? about.map((m) => m.sender) : (cause2.facts.unanswered ?? []).map((u) => u.recipient);
111536
+ return recipients.every((r) => back.includes(r)) ? base2 : base2 + 1;
111537
+ }
111538
+ function pickWork(candidates, now) {
111539
+ const classOf = /* @__PURE__ */ new Map();
111540
+ for (const c of candidates) {
111541
+ const hold = c.needsReply ? HOLD_QUESTION_MS : HOLD_OTHER_MS;
111542
+ const k2 = c.decides || now - c.dueAt > hold ? 1 : c.needsReply ? 2 : 3;
111543
+ const was = classOf.get(c.key);
111544
+ if (was === void 0 || k2 < was) classOf.set(c.key, k2);
111545
+ }
111546
+ let best = null;
111547
+ let bestClass = 4;
111548
+ for (const [key, k2] of classOf) {
111549
+ if (k2 < bestClass) {
111550
+ best = key;
111551
+ bestClass = k2;
111552
+ }
111553
+ }
111554
+ return best;
111555
+ }
111556
+ function flowsDownTo(f) {
111557
+ if (f.fromPerson) return true;
111558
+ if (!f.fromSession || f.toOrchestrator) return false;
111559
+ return f.kind === "task" || f.kind === "message" && f.fromParent;
111560
+ }
111561
+
111562
+ // packages/agent-host/src/mail/delegation.ts
111563
+ function delegationRecorder(mail, turns) {
111564
+ return (callerId, childId, origin, task) => {
111565
+ const written = mail.task(mail.sessionAddress(callerId), mail.sessionAddress(childId), { text: task, origin: delegationOrigin(origin), delivered: true });
111566
+ turns?.attach(childId, written.id, callerId);
111567
+ };
111568
+ }
111569
+ function delegationOrigin(origin) {
111570
+ return origin === "start_agent" || origin === "agent_session" || origin === "send_to_session" || origin === "create_session" || origin === "ask_project" ? origin : "send_to_session";
111571
+ }
111572
+ function personWorkRecorder(mail) {
111573
+ return (sessionId, text3, fromScript) => {
111574
+ mail.task(PERSON, mail.sessionAddress(sessionId), { text: text3, origin: fromScript ? "script" : "create_session", delivered: true });
111575
+ };
111576
+ }
111577
+ function personOwes(mail, records) {
111578
+ return (sessionId) => records.newestTask(PERSON, mail.sessionAddress(sessionId)) !== void 0;
111579
+ }
111580
+ function personReportRecorder(mail, records, waitingOn = () => 0) {
111581
+ return (sessionId, draft) => {
111582
+ const task = records.newestTask(PERSON, mail.sessionAddress(sessionId));
111583
+ if (!task) return false;
111584
+ const owed = draft.then === "review" && draft.outcome === "finished" ? waitingOn(sessionId) : 0;
111585
+ try {
111586
+ mail.report(task.id, turnReport(owed > 0 ? { ...draft, outcome: "paused", then: "running" } : draft, owed));
111587
+ return true;
111588
+ } catch (e) {
111589
+ if (refusalReason(e) !== void 0) return false;
111590
+ throw e;
111591
+ }
111592
+ };
111593
+ }
111594
+ var TURN_ENDED = "[Centralu] The session ended its turn. Its last response says whether the work is done.";
111595
+ var ANSWER_KEPT = 600;
111596
+ function answerRecorder(mail, records, turns, waitingOn = () => 0) {
111597
+ return (callerId, childId, how) => {
111598
+ const owner = mail.sessionAddress(callerId);
111599
+ const child = mail.sessionAddress(childId);
111600
+ const now = Date.now();
111601
+ const reports = records.unreadFor(owner).filter((p) => p.message.kind === "report" && reportOnTaskTo(records, p.message, owner, child)).map((p) => p.message.id);
111602
+ records.markDelivered(owner, reports, now, null);
111603
+ records.markRead(owner, reports, now);
111604
+ const task = records.newestTask(owner, child);
111605
+ if (!how || !task) return;
111606
+ const read = `read_session(sessionId="${childId}")`;
111607
+ const waiting = how.ok ? waitingOn(callerId, childId) : 0;
111608
+ const startedAt = turns?.latest(childId)?.startedAt;
111609
+ const written = mail.report(task.id, {
111610
+ ...startedAt !== void 0 ? { startedAt } : {},
111611
+ ...waiting > 0 ? { outcome: "waiting", waitingOn: waiting } : { outcome: how.ok ? "finished" : "failed" },
111612
+ body: how.ok ? `${TURN_ENDED}
111613
+ id: ${childId}
111614
+
111615
+ Its last response is that session's own words, so it is not repeated here: ${read} reads it.` : `[Centralu] The assigned work did not finish.
111616
+ id: ${childId}
111617
+
111618
+ Its tool's error, by kind: ${how.error}. Its words are its tool's, not Centralu's, so they are not repeated here: ${read} shows them.`,
111619
+ ...how.ok ? { answer: how.text.length > ANSWER_KEPT ? `${how.text.slice(0, ANSWER_KEPT)}\u2026` : how.text } : { error: how.error }
111620
+ });
111621
+ if ("message" in written) {
111622
+ records.markDelivered(owner, [written.message.id], now, null);
111623
+ records.markRead(owner, [written.message.id], now);
111624
+ }
111625
+ };
111626
+ }
111627
+ function reportOnTaskTo(records, report, owner, child) {
111628
+ const task = report.replyTo === null ? void 0 : records.message(report.replyTo);
111629
+ return task?.kind === "task" && task.sender === owner && records.recipient(task.id, child) !== void 0;
111630
+ }
111631
+ function turnRecorder(mail, records, turns) {
111632
+ return (sessionId, turn) => {
111633
+ const child = mail.sessionAddress(sessionId);
111634
+ const taskId = turn.ownerId && (turn.by === "work" || turn.added) ? records.newestTask(mail.sessionAddress(turn.ownerId), child)?.id ?? null : null;
111635
+ const personWork = () => turn.ownerId ? null : records.newestTask(PERSON, child)?.id ?? null;
111636
+ const record2 = (by, messageId, owner) => turn.again ? turns.restate(sessionId, by, messageId, owner) : turns.beginOther(sessionId, by, messageId, owner);
111637
+ if (turn.by === "person") record2("person", null);
111638
+ else if (turn.added) record2("person", taskId, turn.ownerId);
111639
+ else if (turn.source === "report") record2("report", null);
111640
+ else if (turn.source === "app") record2("app", null);
111641
+ else {
111642
+ const person = taskId === null ? personWork() : null;
111643
+ record2(turn.cli && person === null ? "cli" : "caller", taskId ?? person, turn.ownerId);
111644
+ }
111645
+ };
111646
+ }
111647
+ function outcomeOf(draft) {
111648
+ if (draft.then === "dropped") return { outcome: "stopped", reason: "taken_over" };
111649
+ const word = draft.outcome;
111650
+ const outcome = word === "finished" || word === "settled" ? "finished" : word === "failed" || word === "limit_reached" ? "failed" : "stopped";
111651
+ return { outcome, ...word !== outcome ? { reason: word } : {} };
111652
+ }
111653
+ function turnReport(draft, waitingOn) {
111654
+ const base2 = { body: draft.body, ...draft.answer ? { answer: draft.answer } : {}, ...draft.error ? { error: draft.error } : {}, ...draft.personNote ? { personNote: true } : {} };
111655
+ if (draft.then === "running" && draft.paused) return { ...base2, outcome: "paused", paused: draft.paused };
111656
+ if (draft.then === "running") return { ...base2, outcome: "waiting", waitingOn: Math.max(1, waitingOn) };
111657
+ return { ...base2, ...outcomeOf(draft) };
111658
+ }
111659
+ function askedReportBody(draft) {
111660
+ return draft.outcome === "finished" ? "[Centralu] The orchestrator ended its turn on your request. What it sent you in reply is its answer; if it sent nothing, it had none to give. Its other words in that turn are not passed on." : `[Centralu] The orchestrator's turn on your request did not finish (${draft.outcome}). What it sent you in reply, if anything, has reached you. Ask again, or ask the person.`;
111661
+ }
111662
+ function reportedWork(records, turns, owner, child, childId) {
111663
+ const turnWork = turns.latest(childId)?.messageId;
111664
+ const work = turnWork == null ? void 0 : records.message(turnWork);
111665
+ if (work && work.sender === owner && records.recipient(work.id, child)) {
111666
+ if (work.kind === "task") return { work, request: false };
111667
+ if (work.kind === "message") return { work, request: true };
111668
+ }
111669
+ const task = records.newestTask(owner, child);
111670
+ return task && { work: task, request: false };
111671
+ }
111672
+ function reportRecorder(mail, records, turns, waitingOn = () => 0) {
111673
+ return (ownerId, childId, draft) => {
111674
+ const owner = mail.sessionAddress(ownerId);
111675
+ const child = mail.sessionAddress(childId);
111676
+ const found = reportedWork(records, turns, owner, child, childId);
111677
+ if (!found) return "none";
111678
+ if (draft.archiving) return "closed";
111679
+ const { work, request } = found;
111680
+ let written;
111681
+ try {
111682
+ const { answer: _answer, ...unanswered2 } = draft;
111683
+ const startedAt = turns.latest(childId)?.startedAt;
111684
+ const report = request ? { ...turnReport(unanswered2, 0), body: askedReportBody(draft) } : turnReport(draft, waitingOn(ownerId, childId));
111685
+ written = mail.report(work.id, startedAt === void 0 ? report : { ...report, startedAt });
111686
+ } catch (e) {
111687
+ if (refusalReason(e) !== void 0) return "none";
111688
+ throw e;
111689
+ }
111690
+ const seq = turns.latest(childId)?.seq;
111691
+ if (request && seq !== void 0 && draft.then !== "running") mail.requestTurnEnded(child, owner, seq);
111692
+ if ("settledAlready" in written || written.duplicate) return "closed";
111693
+ if (draft.delivery === "wake") return "mailed";
111694
+ if (!written.first && !draft.ownerInTurn) return "closed";
111695
+ const reportId = written.message.id;
111696
+ return {
111697
+ keep: true,
111698
+ settle: () => {
111699
+ const now = Date.now();
111700
+ records.markDelivered(owner, [reportId], now, null);
111701
+ records.markRead(owner, [reportId], now);
111702
+ }
111703
+ };
111704
+ };
111705
+ }
111706
+
111317
111707
  // packages/agent-host/src/sessions/child-ownership.ts
111318
111708
  import { randomUUID as randomUUID12 } from "node:crypto";
111319
111709
 
@@ -111416,6 +111806,8 @@ var ChildOwnership = class {
111416
111806
  starters = /* @__PURE__ */ new Map();
111417
111807
  /** Turns the tool started by itself, no message behind them (`turnStarted`, #1576); memory only, a swap forgets it */
111418
111808
  cliTurns = /* @__PURE__ */ new Set();
111809
+ /** Sessions whose running turn an update cut and Centralu continued (#1966): the turn's end carries it, once. Memory only */
111810
+ continuedCuts = /* @__PURE__ */ new Set();
111419
111811
  lastBy = /* @__PURE__ */ new Map();
111420
111812
  /** The messages given each session's tool that no turn has taken yet (#1639), oldest first; read back from the rows once per host (`held`) */
111421
111813
  entries = /* @__PURE__ */ new Map();
@@ -112004,6 +112396,7 @@ var ChildOwnership = class {
112004
112396
  this.starters.delete(sessionId);
112005
112397
  this.cliTurns.delete(sessionId);
112006
112398
  this.takenOver.delete(sessionId);
112399
+ if (this.continuedCuts.delete(sessionId)) starter.cut = true;
112007
112400
  if (cut3) this.dropInputOrigins(sessionId);
112008
112401
  this.noteTurnEnd(sessionId);
112009
112402
  this.spendCarried(sessionId);
@@ -112047,6 +112440,14 @@ var ChildOwnership = class {
112047
112440
  lastTurnBy(sessionId) {
112048
112441
  return this.lastBy.get(sessionId);
112049
112442
  }
112443
+ /** The turn running was cut by an update and continued (#1966): its report says so in one line */
112444
+ noteContinuedCut(sessionId) {
112445
+ this.continuedCuts.add(sessionId);
112446
+ }
112447
+ /** The Continue did not go in after all: the turn's end is a stopped turn's, not a continued one's */
112448
+ dropContinuedCut(sessionId) {
112449
+ this.continuedCuts.delete(sessionId);
112450
+ }
112050
112451
  /** Whose the running turn is, when one is recorded */
112051
112452
  starterOf(sessionId) {
112052
112453
  return this.starters.get(sessionId);
@@ -112584,7 +112985,7 @@ ${machineLine(machine)}` };
112584
112985
  var isFromTheFuture = (at2, now) => at2 > now;
112585
112986
 
112586
112987
  // packages/agent-host/src/dev-services/store/usage-history.ts
112587
- var HOUR_MS = 60 * 6e4;
112988
+ var HOUR_MS2 = 60 * 6e4;
112588
112989
  var BUCKET_COLUMNS = `hour, session_id AS sessionId, project_id AS projectId, tool, model, machine, requests, input,
112589
112990
  cache_read AS cacheRead, cache_write AS cacheWrite, cache_write_5m AS cacheWrite5m, cache_write_1h AS cacheWrite1h,
112590
112991
  output, rebuilds, rebuild_tokens AS rebuildTokens, causes`;
@@ -112723,7 +113124,7 @@ function mergeCauses(a, b2) {
112723
113124
  var OTHERS_KEY = "*";
112724
113125
  var MAX_KEYS = 50;
112725
113126
  var NO_PROJECT_KEY = "none";
112726
- var HOUR_MS2 = 60 * 6e4;
113127
+ var HOUR_MS3 = 60 * 6e4;
112727
113128
  function dayStart(ms3) {
112728
113129
  const d2 = new Date(ms3);
112729
113130
  d2.setHours(0, 0, 0, 0);
@@ -112827,7 +113228,7 @@ ${r.scope ?? ""}`;
112827
113228
  }
112828
113229
  function usageHistory(usage, p, names) {
112829
113230
  const firstHour = usage.firstHour(p.tool);
112830
- const fromHour = Math.floor(p.from / HOUR_MS2) * HOUR_MS2;
113231
+ const fromHour = Math.floor(p.from / HOUR_MS3) * HOUR_MS3;
112831
113232
  return {
112832
113233
  by: p.by,
112833
113234
  readings: readingSeries(usage.readings(p.from, p.to, p.tool)),
@@ -113102,9 +113503,9 @@ Pass a machine's name as machine to create_project or add_project.` };
113102
113503
  remove_app: ({ app: app2, project: project2 }, call2) => appUpkeepCommand({ action: "remove", app: app2, ...trimmed(project2) ? { project: trimmed(project2) } : {} }, call2),
113103
113504
  share_app: ({ app: app2, project: project2, shared }, call2) => trustCommand({ action: "share_app", app: app2, ...trimmed(project2) ? { project: trimmed(project2) } : {}, shared }, call2),
113104
113505
  // The preferences (#1219): the flow writes each answer; set_preferences names what it did not find to change
113105
- get_preferences: (_args, call2) => preferencesCommand({ action: "get" }, call2),
113506
+ get_preferences: (args, call2) => preferencesCommand({ action: "get", ...trimmed(args.themeFile) ? { themeFile: trimmed(args.themeFile) } : {} }, call2),
113106
113507
  apply_agent_versions: (_args, call2) => preferencesCommand({ action: "apply_agent_versions" }, call2),
113107
- set_preferences: (changes, call2) => Object.values(changes).some((v) => v !== void 0 && !(typeof v === "object" && Object.values(v).every((x) => x === void 0))) ? preferencesCommand({ action: "set", changes }, call2) : { text: "Nothing to change: give at least one of theme, textSize, bodyFont, codeFont, lineHeight, idleSleepMinutes, autoUpdate, moveIdleSessionsToNewAgentCli, sendWithModifierEnter, foldedGroups, notifications, autoCompact.", isError: true },
113508
+ set_preferences: (changes, call2) => Object.values(changes).some((v) => v !== void 0 && !(typeof v === "object" && Object.values(v).every((x) => x === void 0))) ? preferencesCommand({ action: "set", changes }, call2) : { text: "Nothing to change: give at least one of theme, textSize, bodyFont, codeFont, lineHeight, idleSleepMinutes, autoUpdate, moveIdleSessionsToNewAgentCli, sendWithModifierEnter, foldedGroups, notifications, autoCompact, agentModels, themeFile, deleteThemeFile.", isError: true },
113108
113509
  async handoff_session(args, { tools, caller, signal }) {
113109
113510
  const sessionId = args.sessionId?.trim() ?? "";
113110
113511
  const waitFor = trimmed(args.waitFor);
@@ -114818,7 +115219,7 @@ var AgentSessionActions = class {
114818
115219
  const seat = caller?.projectId ? "project" : caller?.kind === "orchestrator" ? "orchestrator" : null;
114819
115220
  const owns = seat === "orchestrator" ? child?.askedBy === callerId : startedHere || !deletingWithoutWorktree && this.host.autoBoundaryOpen(callerId, "session ownership");
114820
115221
  if (!caller || !seat || !child || !owns || child.projectId !== caller.projectId) {
114821
- return { ok: false, error: `${opts.sessionId} is not a session this session started. agent_session works only on sessions start_agent started here.` };
115222
+ return { ok: false, error: `${opts.sessionId} is not a session this session started. agent_session works only on sessions start_agent started here.${child ? "" : staleSessionHint(this.host, opts.sessionId, (s) => s.projectId === caller?.projectId && s.id !== callerId && (seat !== "orchestrator" || s.askedBy === callerId))}` };
114822
115223
  }
114823
115224
  if (opts.sessionId === callerId) return this.flow.agentAtRest(child);
114824
115225
  if (opts.action === "answer") return this.host.answerRoutedCard(callerId, child.id, opts.card, opts.text ?? "");
@@ -115093,8 +115494,6 @@ var RemoteAgents = class {
115093
115494
  toldDown = /* @__PURE__ */ new Map();
115094
115495
  /** The machines whose link this hub's own update or rollback holds, until it is back (#1618, `linkChanged`) */
115095
115496
  updateHolds = /* @__PURE__ */ new Set();
115096
- /** 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 */
115097
- toldCut = /* @__PURE__ */ new Set();
115098
115497
  /** The cards of a caller's turn its caller was told of, once each (`<session>\0<turn>\0<request>`, #676) */
115099
115498
  toldCards = /* @__PURE__ */ new Set();
115100
115499
  /** Whose each child's turn is, as settled in this host (`Whose`); one not here is read from its record (`whoseOf`) */
@@ -115232,7 +115631,7 @@ var RemoteAgents = class {
115232
115631
  if (!reader || this.pending.has(rec.sessionId)) return;
115233
115632
  await this.verify(rec, reader);
115234
115633
  const report = this.host.children.linkedReports.get(rec.sessionId);
115235
- if (!report || report.orchestratorId !== rec.callerId || report.paused || !this.gateNow(rec).owned) return;
115634
+ if (!report || report.orchestratorId !== rec.callerId || report.paused && !report.busy || !this.gateNow(rec).owned) return;
115236
115635
  const now = await reader.read(rec.sessionId, 1);
115237
115636
  if (!now.ok || this.host.children.linkedReports.get(rec.sessionId) !== report || !this.gateNow(rec).owned) return;
115238
115637
  this.deferred.delete(rec.sessionId);
@@ -116103,39 +116502,15 @@ Reason, by kind: ${errorKind(why2)} (its words are an error's, kept in this noti
116103
116502
  const held3 = this.deferred.delete(rec.sessionId);
116104
116503
  if (this.whoseOf(rec) === "unknown" && !this.pending.has(rec.sessionId)) await this.verify(rec, reader);
116105
116504
  const report = this.host.children.linkedReports.get(rec.sessionId);
116106
- if (!report || report.orchestratorId !== rec.callerId || report.paused || !this.gateNow(rec).owned) continue;
116505
+ if (!report || report.orchestratorId !== rec.callerId || report.paused && !report.busy || !this.gateNow(rec).owned) continue;
116107
116506
  if (!report.busy && !missed && !held3) continue;
116108
116507
  const now = await reader.read(rec.sessionId, 1);
116109
116508
  if (!now.ok || this.host.children.linkedReports.get(rec.sessionId) !== report || !this.gateNow(rec).owned) continue;
116110
116509
  if (inTurn(now.state ?? "idle")) {
116111
116510
  report.busy = true;
116112
- this.tellContinued(rec);
116113
116511
  } else this.host.settleLinkedTurn(rec.sessionId);
116114
116512
  }
116115
116513
  }
116116
- /**
116117
- * A child whose turn an update of its machine cut, found in a turn again once the link is back: the host that update
116118
- * started continued it (#1618). Its caller is told so once, and that the report it is owed is that turn's end, so it
116119
- * neither waits on a link that came back nor continues the child by hand a second time.
116120
- *
116121
- * Whatever the order of the Continue and the reconnect (#1626 review): the machine's host names each turn it continues
116122
- * from its start on, before it answers any hello, not once the Continue is in (`SessionManager.updateContinued`), so a
116123
- * link back while the Continue is still on its way is told it too
116124
- */
116125
- tellContinued(rec) {
116126
- const at2 = this.host.linkedReader?.continuedAfterUpdate?.(rec.sessionId);
116127
- if (at2 == null || this.toldCut.has(`${rec.sessionId}@${at2}`)) return;
116128
- this.toldCut.add(`${rec.sessionId}@${at2}`);
116129
- const rest = "and Centralu continued it there on the new version. Its report comes when that turn ends; nothing is needed from you now.";
116130
- void this.host.tellSession(
116131
- rec.callerId,
116132
- `[Centralu] An update of Centralu on its machine cut the turn "${rec.name}" [${rec.sessionId}] was running there, ${rest}`,
116133
- { sessionId: rec.sessionId, name: rec.name },
116134
- true,
116135
- // What the model reads (#1186): all of it but the name, which is not Centralu's words (#120)
116136
- `[Centralu] An update of Centralu on its machine cut the turn the session [${rec.sessionId}] was running there, ${rest}`
116137
- );
116138
- }
116139
116514
  /**
116140
116515
  * Whether a message the child took was sent by this hub over this link (#1038 review): the machine records a hub's
116141
116516
  * instruction from `HUB_SENDER`, which reaches the hub qualified with that machine's id. Another machine's prefix is no
@@ -116430,13 +116805,13 @@ var NOT_READ = "(not read: the session's last turn was the person's; read_sessio
116430
116805
  function outcomeWord(work, stop2, ending) {
116431
116806
  if (work && "tookOver" in work) return "took_over";
116432
116807
  if (work && "swapped" in work) return "swapped";
116433
- if (work && "resumed" in work) return "resumed";
116434
116808
  if (work && ("machineRemoved" in work || "linkLost" in work)) return "stopped";
116435
116809
  if (work && ("paused" in work || "waitingOn" in work)) return "paused";
116436
116810
  if (work && "settled" in work) return "settled";
116437
116811
  if (stop2?.reason === "network") return "network";
116438
116812
  if (stop2?.reason === "interrupted") return "interrupted";
116439
116813
  if (stop2?.reason === "quit") return "quit";
116814
+ if (stop2?.reason === "shutdown") return "shutdown";
116440
116815
  if (ending.type === "error") return "failed";
116441
116816
  if (ending.type === "limit_reached") return "limit_reached";
116442
116817
  if (ending.type === "turn_complete") return "finished";
@@ -116456,6 +116831,8 @@ function personReportLine(outcome, tasks) {
116456
116831
  return "[Centralu] The session you started stopped before finishing: its tool interrupted the turn.";
116457
116832
  case "quit":
116458
116833
  return "[Centralu] The session you started stopped before finishing: Centralu quit or restarted during its turn.";
116834
+ case "shutdown":
116835
+ return `[Centralu] The session you started stopped before finishing: ${SHUTDOWN_WORDS} during its turn.`;
116459
116836
  case "paused":
116460
116837
  return `[Centralu] The session you started paused: its turn ended while its own background work is still running (${tasks} task${tasks === 1 ? "" : "s"}).`;
116461
116838
  default:
@@ -116481,17 +116858,17 @@ function machineRemovedReportLine(machine, paused, name = STORED) {
116481
116858
  function swappedReportLine() {
116482
116859
  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";
116483
116860
  }
116484
- function resumedReportLine(owed) {
116485
- 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");
116486
- }
116487
116861
  function tookOverReportLine(paused, name = STORED) {
116488
116862
  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.
116489
116863
  ` : "The work you assigned was still running, and no report of it follows.\n");
116490
116864
  }
116491
116865
  function personPartLine(turn) {
116492
- if (turn?.added) return "The person added an instruction to this task during this turn.\n";
116493
- if (turn?.wrote) return "The person wrote in this session during this turn.\n";
116494
- return "";
116866
+ const cut3 = turn?.cut ? "An update of Centralu cut this turn and Centralu continued it.\n" : "";
116867
+ if (turn?.added) return `${cut3}The person added an instruction to this task during this turn.
116868
+ `;
116869
+ if (turn?.wrote) return `${cut3}The person wrote in this session during this turn.
116870
+ `;
116871
+ return cut3;
116495
116872
  }
116496
116873
  function settledReportLine(tasks, name = STORED) {
116497
116874
  const clean = tasks.every((t) => t.status === "completed");
@@ -116677,26 +117054,12 @@ var ChildReports = class {
116677
117054
  const asker = requester ?? m?.askedBy ?? void 0;
116678
117055
  if (!m || !asker || asker === sessionId || !stop2 || stop2.reason === "reconnecting" || this.host.handingOver) return;
116679
117056
  const askerInfo = this.host.meta.get(asker);
116680
- if (!askerInfo || askerInfo.interruption?.reason === "quit") return;
117057
+ if (!askerInfo || wentWithItsProcess(askerInfo.interruption)) return;
116681
117058
  console.error(`[sessions] ${sessionId.slice(0, 8)} stopped (${stop2.reason}) in a turn no report was owed for; telling ${asker.slice(0, 8)}, which started it`);
116682
117059
  const { name, project: project2 } = this.reportedAs(m);
116683
117060
  const { preview, seq } = this.stoppedTurnAnswer(sessionId);
116684
117061
  await this.deliverReport(asker, sessionId, name, project2, preview, { type: stop2.reason === "network" ? "error" : "state_change" }, void 0, seq);
116685
117062
  }
116686
- /**
116687
- * A turn of this session that a linked hub's update cut, and this host continued (#1618): its requester hears it once,
116688
- * 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
116689
- * owed, if one is, stays owed: the continued turn's end is it, so it is neither lost nor sent twice
116690
- */
116691
- async tellCutByUpdate(sessionId, requester) {
116692
- const m = this.host.meta.get(sessionId);
116693
- if (!m || !this.host.meta.has(requester) || this.host.handingOver) return;
116694
- const { name, project: project2 } = this.reportedAs(m);
116695
- const owed = this.host.children.awaitingReport.get(sessionId) === requester;
116696
- console.error(`[sessions] ${sessionId.slice(0, 8)}: an update cut its turn and it was continued; telling ${requester.slice(0, 8)}`);
116697
- const { preview, seq } = this.stoppedTurnAnswer(sessionId);
116698
- await this.deliverReport(requester, sessionId, name, project2, preview, { type: "state_change" }, { resumed: true, owed }, seq);
116699
- }
116700
117063
  /**
116701
117064
  * The words of the turn a stop cut (a quit, the network, an update), as a notice of it quotes them: those said so far, or none, never the turn
116702
117065
  * before's (#1604, #1946). A turn not seen to begin (one an older build ran) reads as the last response does
@@ -116758,13 +117121,14 @@ ${read} shows what the person and the session did since.`;
116758
117121
  /** Reports the end of a turn the orchestrator asked to hear about, as one message in its session (#166) */
116759
117122
  async deliverReport(orchestratorId, sessionId, name, project2, preview, ending, work, answerSeq, part) {
116760
117123
  const stop2 = this.host.meta.get(sessionId)?.interruption;
116761
- const cutShort = stop2?.reason === "network" || stop2?.reason === "interrupted" || stop2?.reason === "quit";
116762
- const outcomeOf3 = (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) : stop2?.reason === "network" ? `[Centralu] The assigned work stopped before finishing: a network error ended its turn.
117124
+ const cutShort = stop2?.reason === "network" || stop2?.reason === "interrupted" || wentWithItsProcess(stop2);
117125
+ const outcomeOf3 = (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) : stop2?.reason === "network" ? `[Centralu] The assigned work stopped before finishing: a network error ended its turn.
116763
117126
  ` : stop2?.reason === "interrupted" ? `[Centralu] The assigned work stopped before finishing: its tool interrupted the turn.
116764
117127
  ` : stop2?.reason === "quit" ? `[Centralu] The assigned work stopped before finishing: Centralu quit or restarted during its turn.
117128
+ ` : stop2?.reason === "shutdown" ? `[Centralu] The assigned work stopped before finishing: ${SHUTDOWN_WORDS} during its turn.
116765
117129
  ` : ending.type === "error" ? `[Centralu] The assigned work failed.
116766
117130
  ` : ending.type === "limit_reached" ? `[Centralu] The assigned work stopped after hitting a usage limit.
116767
- ` : ending.type === "turn_complete" ? `[Centralu] The assigned work finished.
117131
+ ` : ending.type === "turn_complete" ? `${TURN_ENDED}
116768
117132
  ` : `[Centralu] The assigned work stopped before finishing.
116769
117133
  `;
116770
117134
  const outcome = outcomeOf3(STORED) + personPartLine(part);
@@ -116777,23 +117141,24 @@ ${read} shows what the person and the session did since.`;
116777
117141
  const cleanUp = !work && !stop2 && ending.type === "turn_complete" ? this.cleanUpWords(orchestratorId, sessionId) : "";
116778
117142
  const read = `read_session(sessionId="${sessionId}"${answerSeq !== void 0 ? `, around=${answerSeq}` : ""})`;
116779
117143
  const kind = message ? stop2?.reason === "network" ? "network" : errorKind(message) : void 0;
116780
- const answered = preview !== "" && preview !== NOT_READ;
117144
+ const answered = preview !== "" && preview !== NOT_READ && preview !== MACHINE_UNREACHABLE && preview !== IN_PAUSE_REPORT;
117145
+ const asRead = !!work && ("linkLost" in work || "machineRemoved" in work) && work.asRead === true;
117146
+ const asReadWords = asRead ? ", as it was last read there, before its machine went out of reach" : "";
116781
117147
  const forModel = (inThread) => outcomeOf3(FOR_MODEL) + personPartLine(part) + `id: ${sessionId}
116782
117148
 
116783
117149
  ` + (kind ? `Its tool's error, by kind: ${kind}. Its words are its tool's, not Centralu's, so they are not repeated here: ${read} shows them.
116784
117150
 
116785
- ` : "") + resume + (inThread && answered ? `Its last response is that session's own words: it is the message above in this thread, and ${read} reads more.` : `Its last response is that session's own words, so it is not repeated here: ${read} reads it${preview ? "" : " (it had none)"}.`) + cleanUp;
117151
+ ` : "") + resume + (inThread && answered ? `Its last response${asReadWords} is that session's own words: it is the message above in this thread, and ${read} reads more.` : preview === MACHINE_UNREACHABLE ? `Its last response could not be read: its machine could not be reached. ${read} reads it once that machine is back.` : `Its last response${asReadWords} is that session's own words, so it is not repeated here: ${read} reads it${preview ? "" : " (it had none)"}.`) + cleanUp;
116786
117152
  const stored = outcome + `Session: ${frameField(name)}
116787
117153
  id: ${sessionId}
116788
117154
  Project: ${frameField(project2)}
116789
117155
 
116790
- ` + failure3 + resume + `Last response:
117156
+ ` + failure3 + resume + `Last response${asReadWords}:
116791
117157
  ${preview || "(no content)"}
116792
117158
 
116793
117159
  If you need more, you can read that session's recent conversation with read_session.` + cleanUp;
116794
117160
  const holding = !!work && "paused" in work && !("tookOver" in work) && !("machineRemoved" in work) && !("linkLost" in work);
116795
117161
  const handedOn = !!work && "waitingOn" in work;
116796
- const goesOn = !!work && "resumed" in work;
116797
117162
  const reported = this.toMailbox(orchestratorId, sessionId, {
116798
117163
  outcome: outcomeWord(work, stop2, ending),
116799
117164
  body: forModel(true),
@@ -116801,7 +117166,7 @@ If you need more, you can read that session's recent conversation with read_sess
116801
117166
  ...kind ? { error: kindWord(kind) } : {},
116802
117167
  ...holding ? { paused: work.paused.length } : {},
116803
117168
  ...part?.wrote || part?.added ? { personNote: true } : {},
116804
- then: holding || handedOn || goesOn ? "running" : "review"
117169
+ then: holding || handedOn ? "running" : "review"
116805
117170
  });
116806
117171
  if (reported === "mailed" || reported === "closed") return;
116807
117172
  await this.tellSession(
@@ -116828,7 +117193,7 @@ If you need more, you can read that session's recent conversation with read_sess
116828
117193
  const others = ownAgents(this.host, parentId).filter((s) => s.id !== childId && agentState(this.host, s) === "finished").length;
116829
117194
  return `
116830
117195
  ${cleanUpLine(childId, this.agentCall(parentId, { sessionId: childId, action: "delete" }))}` + (others ? `
116831
- ${others} other finished session${others === 1 ? "" : "s"} you started remain${others === 1 ? "s" : ""}: ${this.agentCall(parentId, { action: "list" })} shows them.` : "");
117196
+ ${others} other session${others === 1 ? "" : "s"} you started ${others === 1 ? "has" : "have"} ended ${others === 1 ? "its turn" : "their turns"} and remain${others === 1 ? "s" : ""}: ${this.agentCall(parentId, { action: "list" })} shows them.` : "");
116832
117197
  }
116833
117198
  /**
116834
117199
  * An agent_session call a report names, in the form its receiver calls it (#850): the orchestrator reaches agent_session
@@ -116895,6 +117260,10 @@ sourceSessionId: ${from.sessionId}`;
116895
117260
  var HELD_NOTICES_MAX = 20;
116896
117261
 
116897
117262
  // packages/agent-host/src/sessions/interruption-watch.ts
117263
+ var LOST_TURN_LINE = {
117264
+ quit: "Centralu quit or restarted while this turn was running, so the turn stopped before it finished. Send a message to continue.",
117265
+ shutdown: `This turn stopped before it finished: ${SHUTDOWN_WORDS} while it was running. Send a message to continue.`
117266
+ };
116898
117267
  var InterruptionWatch = class {
116899
117268
  constructor(host) {
116900
117269
  this.host = host;
@@ -116940,19 +117309,20 @@ var InterruptionWatch = class {
116940
117309
  }
116941
117310
  /**
116942
117311
  * The turn went with its process (#698): a start found the session mid-turn with no process left (a full quit, a
116943
- * reboot, a crash the keeper did not survive), or a kept process could not be re-attached. The session is reset to
117312
+ * reboot, a crash the keeper did not survive), or a kept process could not be re-attached. `shutdown`: the start found the
117313
+ * last host recorded the system ending (a shutdown, restart or sign-out, #1397), which is said so rather than as a quit. The session is reset to
116944
117314
  * `idle`, which used to leave no mark at all: the conversation simply stopped. A line now says so, and the
116945
117315
  * interruption puts the session in the inbox with a way to continue.
116946
117316
  */
116947
- markStoppedByQuit(m, announce) {
116948
- m.interruption = { reason: "quit", at: Date.now() };
117317
+ markTurnLost(m, announce, reason = "quit") {
117318
+ m.interruption = { reason, at: Date.now() };
116949
117319
  const notice = {
116950
117320
  type: "notice",
116951
117321
  sessionId: m.id,
116952
117322
  level: "warning",
116953
117323
  from: "Centralu",
116954
117324
  label: "stopped",
116955
- text: "Centralu quit or restarted while this turn was running, so the turn stopped before it finished. Send a message to continue."
117325
+ text: LOST_TURN_LINE[reason]
116956
117326
  };
116957
117327
  const seq = this.host.record.persistMessage(notice, m);
116958
117328
  if (!announce) return;
@@ -117247,11 +117617,150 @@ var MergeChecks = class {
117247
117617
  }
117248
117618
  };
117249
117619
 
117620
+ // packages/agent-host/src/themes.ts
117621
+ import { randomBytes as randomBytes7 } from "node:crypto";
117622
+ import { watch as watch3 } from "node:fs";
117623
+ import { mkdir as mkdir4, readdir as readdir7, readFile as readFile4, rename as rename4, stat as stat7 } from "node:fs/promises";
117624
+ import { basename as basename8, join as join31 } from "node:path";
117625
+ var MAX_FILE_BYTES = 256 * 1024;
117626
+ var SETTLE_MS = 100;
117627
+ function themeSlug(name) {
117628
+ return name.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "theme";
117629
+ }
117630
+ var REMOVED_DIR = ".removed";
117631
+ var ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,59}$/;
117632
+ var ThemeFiles = class {
117633
+ constructor(dir, onChange) {
117634
+ this.dir = dir;
117635
+ this.onChange = onChange;
117636
+ }
117637
+ dir;
117638
+ onChange;
117639
+ watcher = null;
117640
+ timer = null;
117641
+ /** Makes the folder, writes the schema next to the themes, and starts watching */
117642
+ async start() {
117643
+ await mkdir4(this.dir, { recursive: true });
117644
+ await this.writeSchema();
117645
+ try {
117646
+ this.watcher = watch3(this.dir, (_event, name) => {
117647
+ if (name && !String(name).endsWith(".json")) return;
117648
+ if (name === THEME_SCHEMA_FILE) return;
117649
+ if (this.timer) clearTimeout(this.timer);
117650
+ this.timer = setTimeout(() => {
117651
+ this.timer = null;
117652
+ this.onChange();
117653
+ }, SETTLE_MS);
117654
+ });
117655
+ this.watcher.on("error", () => this.stopWatching());
117656
+ } catch {
117657
+ }
117658
+ }
117659
+ close() {
117660
+ this.stopWatching();
117661
+ if (this.timer) clearTimeout(this.timer);
117662
+ this.timer = null;
117663
+ }
117664
+ stopWatching() {
117665
+ this.watcher?.close();
117666
+ this.watcher = null;
117667
+ }
117668
+ /**
117669
+ * The schema an editor uses for completion, written only when it differs — a build that adds
117670
+ * a token updates it, and an unchanged one does not touch the folder (or wake the watcher).
117671
+ */
117672
+ async writeSchema() {
117673
+ const path2 = join31(this.dir, THEME_SCHEMA_FILE);
117674
+ const text3 = `${JSON.stringify(themeFileJsonSchema(), null, 2)}
117675
+ `;
117676
+ const current = await readFile4(path2, "utf8").catch(() => null);
117677
+ if (current !== text3) writeFileWhole(realOs().files, path2, text3);
117678
+ }
117679
+ /** Every `<id>.json` in the folder, read and checked. A file that cannot be read is listed with the reason. */
117680
+ async list() {
117681
+ const names = await readdir7(this.dir).catch(() => []);
117682
+ const entries = [];
117683
+ const ids = names.filter((name) => name.endsWith(".json") && name !== THEME_SCHEMA_FILE).map((name) => name.slice(0, -".json".length)).filter((id3) => ID_PATTERN.test(id3)).sort();
117684
+ for (const id3 of ids) entries.push(await this.read(id3));
117685
+ return entries;
117686
+ }
117687
+ async read(id3) {
117688
+ const path2 = this.pathOf(id3);
117689
+ try {
117690
+ const info = await stat7(path2);
117691
+ if (info.size > MAX_FILE_BYTES) {
117692
+ return { id: id3, path: path2, name: id3, base: "dark", tokens: {}, problems: ["The file is too large to be a theme."], broken: true };
117693
+ }
117694
+ return parseThemeFile(id3, path2, await readFile4(path2, "utf8"));
117695
+ } catch (e) {
117696
+ return { id: id3, path: path2, name: id3, base: "dark", tokens: {}, problems: [`Could not read the file: ${e.message}`], broken: true };
117697
+ }
117698
+ }
117699
+ /** Writes a theme. `id` null makes a new file named after the theme (`My theme` → `my-theme.json`). */
117700
+ async save(id3, content) {
117701
+ const text3 = formatThemeFile(content);
117702
+ if (Buffer.byteLength(text3) > MAX_FILE_BYTES) throw rpcError("The theme is too large to be a theme file.");
117703
+ const target = id3 ?? await this.freeId(content.name);
117704
+ await mkdir4(this.dir, { recursive: true });
117705
+ const names = await readdir7(this.dir).catch(() => []);
117706
+ const clash = names.find((n) => n !== `${target}.json` && n.toLowerCase() === `${target}.json`);
117707
+ if (clash) throw rpcError(`The themes folder has ${clash}, which the list does not show (ids are lowercase); rename or remove it first.`);
117708
+ writeFileWhole(realOs().files, this.pathOf(target), text3);
117709
+ return this.read(target);
117710
+ }
117711
+ /**
117712
+ * Takes a theme file out of the list without destroying it: it moves to `.removed/<id>-<time>-<random>.json`, a place the list
117713
+ * does not look, so a mistaken removal is one move back. The host has no OS trash (Settings' button reaches it through the
117714
+ * shell), so these stay until the person clears the folder. Where it went is returned; null when there was no such file.
117715
+ */
117716
+ async remove(id3) {
117717
+ const from = this.pathOf(id3);
117718
+ if (!await stat7(from).then(() => true, () => false)) return null;
117719
+ const kept = join31(this.dir, REMOVED_DIR);
117720
+ await mkdir4(kept, { recursive: true });
117721
+ const to = join31(kept, `${id3}-${Date.now()}-${randomBytes7(3).toString("hex")}.json`);
117722
+ await rename4(from, to);
117723
+ return to;
117724
+ }
117725
+ /** Copies a file from elsewhere in, under a fresh id if its name is taken. Refuses what does not read as a theme. */
117726
+ async importFrom(source) {
117727
+ const info = await stat7(source);
117728
+ if (!info.isFile()) throw rpcError("That is not a file.");
117729
+ if (info.size > MAX_FILE_BYTES) throw rpcError("The file is too large to be a theme.");
117730
+ const text3 = await readFile4(source, "utf8");
117731
+ const parsed = parseThemeFile("import", source, text3);
117732
+ if (parsed.broken) throw rpcError(parsed.problems[0] ?? "Not a theme file.");
117733
+ const fromName = basename8(source).replace(/\.json$/i, "");
117734
+ const id3 = await this.freeId(parsed.name !== "import" ? parsed.name : fromName);
117735
+ writeFileWhole(realOs().files, this.pathOf(id3), text3);
117736
+ return this.read(id3);
117737
+ }
117738
+ /** The absolute path for an id, for the shell to trash or reveal. Refuses anything that is not an id. */
117739
+ pathOf(id3) {
117740
+ if (!ID_PATTERN.test(id3)) throw rpcError(`Not a theme id: ${id3}`);
117741
+ return join31(this.dir, `${id3}.json`);
117742
+ }
117743
+ async freeId(name) {
117744
+ const base2 = themeSlug(name);
117745
+ for (let n = 1; n < 1e3; n++) {
117746
+ const id3 = n === 1 ? base2 : `${base2}-${n}`;
117747
+ const taken = await stat7(this.pathOf(id3)).then(
117748
+ () => true,
117749
+ () => false
117750
+ );
117751
+ if (!taken) return id3;
117752
+ }
117753
+ return `${base2}-${randomBytes7(4).toString("hex")}`;
117754
+ }
117755
+ };
117756
+
117250
117757
  // packages/agent-host/src/sessions/window-rpc.ts
117251
117758
  var WINDOW_METHODS = [
117252
117759
  "prefs.get",
117253
117760
  "prefs.set",
117254
117761
  "themes.list",
117762
+ "themes.save",
117763
+ "themes.remove",
117255
117764
  "agents.sleepPolicy",
117256
117765
  "agents.setSleepPolicy",
117257
117766
  "updates.status",
@@ -117311,6 +117820,7 @@ async function callWindow(rpc, method, params) {
117311
117820
  // packages/agent-host/src/sessions/preferences-flow.ts
117312
117821
  var HIDDEN2 = /[\p{Cc}\p{Cf}\u2028\u2029]/u;
117313
117822
  var UPDATE_CARD = 'Turn on "Automatic updates"? From then on this computer checks for a newer Centralu, installs it by itself as soon as one is published, and the desktop window relaunches into it when nothing is busy, without asking again. You can turn it off in Settings, Updates.';
117823
+ var THEME_FILE_MAX_BYTES = 256 * 1024;
117314
117824
  var FOLDED_GROUPS_MAX = 500;
117315
117825
  var SLEEP_CHOICES = 'a whole number of minutes, or "never"';
117316
117826
  function autoCompactText(c) {
@@ -117332,7 +117842,7 @@ var PreferencesFlow = class {
117332
117842
  const rpc = this.rpc;
117333
117843
  if (!rpc) return { ok: false, error: "This host has no window calls to make." };
117334
117844
  try {
117335
- if (req.action === "get") return await this.get(rpc);
117845
+ if (req.action === "get") return await this.get(rpc, req.themeFile);
117336
117846
  if (req.action === "apply_agent_versions") return await this.applyVersions(rpc);
117337
117847
  return await this.set(rpc, sessionId, req.changes, signal);
117338
117848
  } catch (e) {
@@ -117340,7 +117850,7 @@ var PreferencesFlow = class {
117340
117850
  }
117341
117851
  }
117342
117852
  /** Every setting Settings shows, one line each; a part the host cannot read says so, and the rest still answers */
117343
- async get(rpc) {
117853
+ async get(rpc, themeFile) {
117344
117854
  const [prefs, themes2, sleep4, updates2, versions] = await Promise.allSettled([
117345
117855
  callWindow(rpc, "prefs.get", {}),
117346
117856
  callWindow(rpc, "themes.list", {}),
@@ -117360,9 +117870,11 @@ var PreferencesFlow = class {
117360
117870
  "Appearance",
117361
117871
  `- Theme mode: ${p.themeMode}; dark theme ${named(p.themeDark)}; light theme ${named(p.themeLight)}`,
117362
117872
  `- Themes to choose from (set_preferences theme): ${choices.map((c) => `${plainQuoted(c.name)} [${c.id}] (${c.base}${c.file ? ", a theme file" : ""})`).join(", ")}`,
117873
+ ...themes2.status === "fulfilled" ? themeFileLines(themes2.value, themeFile) : [],
117363
117874
  `- Text size: ${p.textSize} (${TEXT_SIZES.join(", ")}; 1 is the default)`,
117364
117875
  `- Body font: ${p.bodyFont ? plainQuoted(p.bodyFont, 200) : "the app's own"}; code font: ${p.codeFont ? plainQuoted(p.codeFont, 200) : "the app's own"}`,
117365
117876
  `- Line height: ${p.lineHeight} (compact, normal, relaxed)`,
117877
+ `- Models for the agents a session starts (set_preferences agentModels): ${agentModelsText(p.agentModels)}`,
117366
117878
  "Notifications (set_preferences notifications)",
117367
117879
  `- Approval needed: ${onOff(notify.approval)}; a session finishes out of sight: ${onOff(notify.done)}; even while the app is focused: ${onOff(notify.whenFocused)}; sound: ${onOff(notify.sound)}; turning the approval one off, or the sound while it notifies, asks the person`,
117368
117880
  "Auto compact (set_preferences autoCompact)",
@@ -117383,12 +117895,19 @@ var PreferencesFlow = class {
117383
117895
  async set(rpc, sessionId, changes, signal) {
117384
117896
  const patch = {};
117385
117897
  let theme = null;
117898
+ const done2 = [];
117899
+ const failed3 = [];
117900
+ const modelWarnings = [];
117901
+ const file2 = changes.themeFile;
117902
+ if (file2 !== void 0) {
117903
+ const bad = themeFileRefusal(file2);
117904
+ if (bad) return { ok: false, error: bad };
117905
+ }
117386
117906
  if (changes.theme !== void 0) {
117387
117907
  const found = await this.theme(rpc, changes.theme);
117388
- if (typeof found === "string") return { ok: false, error: found };
117389
- theme = found;
117390
- patch.themeMode = found.base;
117391
- patch[found.base === "dark" ? "themeDark" : "themeLight"] = found.id;
117908
+ const wanted = changes.theme.trim().toLowerCase();
117909
+ const madeHere = file2 !== void 0 && (file2.name.trim().toLowerCase() === wanted || await this.idOfFile(rpc, file2) === wanted);
117910
+ if (typeof found === "string" && !madeHere) return { ok: false, error: found };
117392
117911
  }
117393
117912
  if (changes.textSize !== void 0) {
117394
117913
  if (!Number.isFinite(changes.textSize)) return { ok: false, error: `textSize is a number: one of ${TEXT_SIZES.join(", ")}.` };
@@ -117400,6 +117919,12 @@ var PreferencesFlow = class {
117400
117919
  if (HIDDEN2.test(value)) return { ok: false, error: `${key} has a control or formatting character in it; give a font family as plain text.` };
117401
117920
  patch[key] = value.trim();
117402
117921
  }
117922
+ if (changes.agentModels !== void 0) {
117923
+ const merged = await this.mergedAgentModels(rpc, changes.agentModels);
117924
+ if (typeof merged === "string") return { ok: false, error: merged };
117925
+ patch.agentModels = merged.models;
117926
+ modelWarnings.push(...merged.warnings);
117927
+ }
117403
117928
  if (changes.lineHeight !== void 0) patch.lineHeight = changes.lineHeight;
117404
117929
  if (changes.sendWithModifierEnter !== void 0) patch.sendWithModifierEnter = changes.sendWithModifierEnter;
117405
117930
  if (changes.foldedGroups !== void 0) {
@@ -117408,13 +117933,35 @@ var PreferencesFlow = class {
117408
117933
  patch.foldedGroups = keys;
117409
117934
  }
117410
117935
  const parsed = UiPreferencesPatch.safeParse(patch);
117411
- if (!parsed.success) return { ok: false, error: `Nothing changed: ${issuesOf(parsed.error)}.` };
117936
+ if (!parsed.success) return { ok: false, error: `${issuesOf(parsed.error)}.` };
117412
117937
  const sleep4 = changes.idleSleepMinutes;
117413
117938
  if (sleep4 !== void 0 && sleep4 !== "never" && !(Number.isInteger(sleep4) && sleep4 > 0)) {
117414
117939
  return { ok: false, error: `idleSleepMinutes is ${SLEEP_CHOICES}.` };
117415
117940
  }
117416
- const done2 = [];
117417
- const failed3 = [];
117941
+ if (file2 !== void 0) {
117942
+ try {
117943
+ const saved = await callWindow(rpc, "themes.save", { id: file2.id?.trim() || null, content: { name: file2.name.trim(), base: file2.base, tokens: file2.tokens } });
117944
+ done2.push(themeFileWritten(saved));
117945
+ } catch (e) {
117946
+ return { ok: false, error: errorMessage(e) };
117947
+ }
117948
+ }
117949
+ let toApply = parsed.data;
117950
+ if (changes.theme !== void 0) {
117951
+ const found = await this.theme(rpc, changes.theme);
117952
+ if (typeof found === "string") failed3.push(`Theme: not picked. ${found}`);
117953
+ else {
117954
+ theme = found;
117955
+ patch.themeMode = found.base;
117956
+ patch[found.base === "dark" ? "themeDark" : "themeLight"] = found.id;
117957
+ const again = UiPreferencesPatch.safeParse(patch);
117958
+ if (again.success) toApply = again.data;
117959
+ else {
117960
+ theme = null;
117961
+ failed3.push(`Theme: not picked. ${issuesOf(again.error)}.`);
117962
+ }
117963
+ }
117964
+ }
117418
117965
  const attempt = async (what, run4) => {
117419
117966
  try {
117420
117967
  done2.push(await run4());
@@ -117424,8 +117971,8 @@ var PreferencesFlow = class {
117424
117971
  };
117425
117972
  if (Object.keys(patch).length) {
117426
117973
  await attempt("Appearance", async () => {
117427
- const now = await callWindow(rpc, "prefs.set", { patch: parsed.data });
117428
- return appearanceText(now, patch, theme);
117974
+ const now = await callWindow(rpc, "prefs.set", { patch: toApply });
117975
+ return [appearanceText(now, patch, theme), ...modelWarnings].join(" ");
117429
117976
  });
117430
117977
  }
117431
117978
  const switches = Object.fromEntries(Object.entries(changes.notifications ?? {}).filter(([, v]) => v !== void 0));
@@ -117461,7 +118008,7 @@ var PreferencesFlow = class {
117461
118008
  if (Object.keys(compact).length) {
117462
118009
  await attempt("Auto compact", async () => {
117463
118010
  const checked2 = UiPreferencesPatch.safeParse({ autoCompact: compact });
117464
- if (!checked2.success) throw new Error(`Nothing changed: ${issuesOf(checked2.error)}.`);
118011
+ if (!checked2.success) throw new Error(`${issuesOf(checked2.error)}.`);
117465
118012
  const now = autoCompactOf(await callWindow(rpc, "prefs.set", { patch: checked2.data }));
117466
118013
  return `Now: ${autoCompactText(now)}. It reaches a running session when its process next starts.`;
117467
118014
  });
@@ -117513,7 +118060,21 @@ var PreferencesFlow = class {
117513
118060
  return `Moving idle sessions to a newly installed agent CLI is ${onOff(now.autoApply)}.`;
117514
118061
  });
117515
118062
  }
117516
- if (!done2.length) return { ok: false, error: `Nothing changed. ${failed3.join(" ")}` };
118063
+ if (changes.deleteThemeFile !== void 0) {
118064
+ const id3 = changes.deleteThemeFile.trim();
118065
+ await attempt("Theme file", async () => {
118066
+ if (!ThemeId.safeParse(id3).success) throw new Error(`${plainQuoted(id3)} is not a theme file id; get_preferences lists them.`);
118067
+ const { removed, keptAt } = await callWindow(rpc, "themes.remove", { id: id3 });
118068
+ if (!removed) throw new Error(`No theme file [${id3}]; get_preferences lists them.`);
118069
+ const now = await callWindow(rpc, "prefs.get", {});
118070
+ const back = {};
118071
+ if (now.themeDark === id3) back.themeDark = "dark";
118072
+ if (now.themeLight === id3) back.themeLight = "light";
118073
+ if (Object.keys(back).length) await callWindow(rpc, "prefs.set", { patch: back });
118074
+ return `Removed the theme file [${id3}]; it is kept at ${plainQuoted(keptAt ?? "", 300)} (move it back to restore it).${Object.keys(back).length ? " A side that showed it is back on its preset." : ""}`;
118075
+ });
118076
+ }
118077
+ if (!done2.length) return { ok: false, error: failed3.join(" ") };
117517
118078
  return { ok: true, text: [...done2, ...failed3.length ? [`Not changed: ${failed3.join(" ")}`] : []].join("\n") };
117518
118079
  }
117519
118080
  /**
@@ -117536,6 +118097,58 @@ var PreferencesFlow = class {
117536
118097
  const kept = now.filter((key) => !drop.has(key) && !add2.includes(key));
117537
118098
  return [...kept, ...add2].slice(-FOLDED_GROUPS_MAX);
117538
118099
  }
118100
+ /**
118101
+ * The saved models with this call's changes merged in (the patch carries the whole map), or the sentence that says what is
118102
+ * wrong. The map steers every difficulty-based start_agent in every project, so the tool must be an agent CLI on this
118103
+ * computer. A model or effort is free text, as Settings' picker takes with "Other…", also when the tool lists its models
118104
+ * (a newer id may not be in the list yet); one the list lacks is saved as typed and said so in a warning. Clearing a
118105
+ * difficulty that was never set changes nothing and leaves no empty entry.
118106
+ */
118107
+ async mergedAgentModels(rpc, change) {
118108
+ const now = (await callWindow(rpc, "prefs.get", {})).agentModels ?? {};
118109
+ const known = (await callWindow(rpc, "agents.detect", {})).map((s) => s.name);
118110
+ const out = { ...now };
118111
+ const warnings = [];
118112
+ for (const [tool, levels] of Object.entries(change)) {
118113
+ if (!known.includes(tool)) return `agentModels: ${plainQuoted(tool, 80)} is not an agent CLI on this computer; agent_status lists them (${known.join(", ") || "none"}).`;
118114
+ const own = { ...out[tool] };
118115
+ let catalog;
118116
+ for (const [level2, choice] of Object.entries(levels)) {
118117
+ if (choice === null) delete own[level2];
118118
+ else if (choice !== void 0) {
118119
+ const model = choice.model.trim();
118120
+ const effort = choice.effort?.trim();
118121
+ if (!model) return "agentModels: a model is empty; give its id as plain text.";
118122
+ if (HIDDEN2.test(model) || HIDDEN2.test(effort ?? "")) return "agentModels has a control or formatting character in a model or effort; give plain text.";
118123
+ if (catalog === void 0) {
118124
+ const r = await callWindow(rpc, "agents.models", { tool }).catch(() => null);
118125
+ catalog = r?.supported && r.models.length ? r.models : null;
118126
+ }
118127
+ if (catalog) {
118128
+ const m = catalog.find((x) => x.id === model);
118129
+ if (!m) warnings.push(`Note: ${plainQuoted(model, 200)} is not in ${tool}'s current list (agent_models); saved as typed.`);
118130
+ else if (effort && m.efforts.length && !m.efforts.includes(effort)) warnings.push(`Note: ${plainQuoted(effort, 80)} is not in the current efforts of ${plainQuoted(model, 200)} (${m.efforts.map((e) => plainQuoted(e, 40)).join(", ")}); saved as typed.`);
118131
+ }
118132
+ own[level2] = effort ? { model, effort } : { model };
118133
+ }
118134
+ }
118135
+ if (Object.keys(own).length) out[tool] = own;
118136
+ else delete out[tool];
118137
+ }
118138
+ return { models: out, warnings };
118139
+ }
118140
+ /** The id a theme file will be written under: the one named, else the one `themes.save` files its name under among the ids taken now */
118141
+ async idOfFile(rpc, file2) {
118142
+ const named = file2.id?.trim().toLowerCase();
118143
+ if (named) return named;
118144
+ const taken = new Set((await callWindow(rpc, "themes.list", {})).map((f) => f.id));
118145
+ const base2 = themeSlug(file2.name.trim());
118146
+ for (let n = 1; n < 1e3; n++) {
118147
+ const id3 = n === 1 ? base2 : `${base2}-${n}`;
118148
+ if (!taken.has(id3)) return id3;
118149
+ }
118150
+ return base2;
118151
+ }
117539
118152
  /** The session header's action: idle sessions on an older agent CLI restart on the installed one, and the busy ones are named */
117540
118153
  async applyVersions(rpc) {
117541
118154
  const r = await callWindow(rpc, "agents.applyVersions", { keepQuiet: true });
@@ -117570,6 +118183,44 @@ function automatic(s) {
117570
118183
  if (!s.auto && !s.autoApply) return "off";
117571
118184
  return s.auto ? "off (checking is on, installing is off)" : "off (installing is on, checking is off)";
117572
118185
  }
118186
+ function themeFileRefusal(file2) {
118187
+ if (file2.id !== void 0 && file2.id.trim() && !ThemeId.safeParse(file2.id.trim()).success) {
118188
+ return `themeFile.id ${plainQuoted(file2.id)} is not a theme id: lowercase letters, digits and dashes, up to 60. Leave it out for a new file.`;
118189
+ }
118190
+ const content = ThemeFileContent.safeParse({ name: file2.name.trim(), base: file2.base, tokens: file2.tokens });
118191
+ if (!content.success) return `themeFile: ${issuesOf(content.error)}.`;
118192
+ if (HIDDEN2.test(file2.name) || Object.entries(file2.tokens).some(([k2, v]) => HIDDEN2.test(k2) || HIDDEN2.test(v))) {
118193
+ return "themeFile has a control or formatting character in its name or tokens; give plain text.";
118194
+ }
118195
+ if (Buffer.byteLength(formatThemeFile(content.data)) > THEME_FILE_MAX_BYTES) return "themeFile is too large to be a theme file; leave out the tokens you do not change.";
118196
+ return null;
118197
+ }
118198
+ function themeFileWritten(f) {
118199
+ const problems = f.problems.length ? ` Problems the reader found, skipped when applied: ${f.problems.map((x) => plainQuoted(x, 200)).join(" ")}` : "";
118200
+ return `Wrote the theme file ${plainQuoted(f.name)} [${f.id}] (${f.base}, ${Object.keys(f.tokens).length} tokens).${problems}`;
118201
+ }
118202
+ function themeFileLines(files2, one2) {
118203
+ const state = (f) => f.broken ? ", unreadable" : f.problems.length ? `, ${f.problems.length} problems` : "";
118204
+ const lines = [
118205
+ files2.length ? `- Theme files (set_preferences themeFile writes one, deleteThemeFile removes one): ${files2.map((f) => `${plainQuoted(f.name)} [${f.id}] (${f.base}, ${Object.keys(f.tokens).length} tokens${state(f)})`).join(", ")}` : "- Theme files: none (set_preferences themeFile writes one)"
118206
+ ];
118207
+ if (one2) {
118208
+ const f = files2.find((x) => x.id === one2.trim());
118209
+ if (!f) lines.push(`- Theme file ${plainQuoted(one2)}: no such file`);
118210
+ else {
118211
+ lines.push(`- Theme file ${plainQuoted(f.name)} [${f.id}], ${f.base}${f.problems.length ? `; problems: ${f.problems.map((x) => plainQuoted(x, 200)).join(" ")}` : ""}`);
118212
+ lines.push(...Object.entries(f.tokens).map(([k2, v]) => ` ${k2}: ${plainQuoted(v, 400)}`));
118213
+ lines.push(` Token keys to set: ${THEME_TOKENS.map((t) => t.key).join(", ")}`);
118214
+ }
118215
+ }
118216
+ return lines;
118217
+ }
118218
+ function agentModelsText(models) {
118219
+ const parts = Object.entries(models ?? {}).flatMap(
118220
+ ([tool, levels]) => Object.entries(levels).flatMap(([level2, c]) => c ? [`${plainQuoted(tool, 80)} ${level2}: ${plainQuoted(c.model, 200)}${c.effort ? ` (${plainQuoted(c.effort, 80)})` : ""}`] : [])
118221
+ );
118222
+ return parts.length ? parts.join("; ") : "none chosen (each project keeps its last choice)";
118223
+ }
117573
118224
  function themeChoices(files2) {
117574
118225
  return [
117575
118226
  ...THEME_PRESETS.map((p) => ({ id: p.id, name: p.name, base: p.base, file: false })),
@@ -117587,6 +118238,7 @@ function appearanceText(now, patch, theme) {
117587
118238
  if ("codeFont" in patch) parts.push(`code font ${now.codeFont ? plainQuoted(now.codeFont, 200) : "the app's own"}`);
117588
118239
  if ("lineHeight" in patch) parts.push(`line height ${now.lineHeight}`);
117589
118240
  if ("sendWithModifierEnter" in patch) parts.push(`send with ${now.sendWithModifierEnter ? "Ctrl+Enter (Enter breaks the line)" : "Enter"}`);
118241
+ if ("agentModels" in patch) parts.push(`models for started agents: ${agentModelsText(now.agentModels)}`);
117590
118242
  if ("foldedGroups" in patch) parts.push(`${now.foldedGroups.length} sidebar ${now.foldedGroups.length === 1 ? "group" : "groups"} folded`);
117591
118243
  return `Now: ${parts.join("; ")}.`;
117592
118244
  }
@@ -118552,7 +119204,7 @@ var HousekeepingFlow = class {
118552
119204
  import { randomUUID as randomUUID18 } from "node:crypto";
118553
119205
  import { existsSync as existsSync15, realpathSync as realpathSync5, rmSync as rmSync4, statSync as statSync13 } from "node:fs";
118554
119206
  import { homedir as homedir7 } from "node:os";
118555
- import { basename as basename8, isAbsolute as isAbsolute5, join as join31, resolve as resolve11 } from "node:path";
119207
+ import { basename as basename9, isAbsolute as isAbsolute5, join as join32, resolve as resolve11 } from "node:path";
118556
119208
 
118557
119209
  // packages/agent-host/src/dev-services/project-folder.ts
118558
119210
  import { lstatSync as lstatSync12, mkdirSync as mkdirSync16, statSync as statSync12 } from "node:fs";
@@ -118565,18 +119217,18 @@ function projectsHome(platform = process.platform, home = homedir6(), env = proc
118565
119217
  if (platform === "win32") return path.win32.join(env.USERPROFILE || home, PROJECTS_DIR);
118566
119218
  return path.posix.join(home, PROJECTS_DIR);
118567
119219
  }
118568
- function refuse4(message) {
119220
+ function refuse5(message) {
118569
119221
  throw rpcError(message);
118570
119222
  }
118571
119223
  function projectFolderTarget(spec, home, platform = process.platform) {
118572
119224
  const p = pathFor(platform);
118573
119225
  const name = spec.name.trim();
118574
119226
  const nameError = folderNameError(name, platform === "win32");
118575
- if (nameError) refuse4(nameError);
119227
+ if (nameError) refuse5(nameError);
118576
119228
  const given = spec.parent?.trim();
118577
- if (given && !p.isAbsolute(given)) refuse4(`The location has to be a full path (${platform === "win32" ? "C:\\Users\\me\\code" : "/home/me/code"}).`);
119229
+ if (given && !p.isAbsolute(given)) refuse5(`The location has to be a full path (${platform === "win32" ? "C:\\Users\\me\\code" : "/home/me/code"}).`);
118578
119230
  const base2 = given || home;
118579
- if (!base2) refuse4("There is no default location here \u2014 give a parent folder.");
119231
+ if (!base2) refuse5("There is no default location here \u2014 give a parent folder.");
118580
119232
  const parent = p.resolve(base2);
118581
119233
  const isDefault = !given;
118582
119234
  if (!isDefault) {
@@ -118584,9 +119236,9 @@ function projectFolderTarget(spec, home, platform = process.platform) {
118584
119236
  try {
118585
119237
  dir = statSync12(parent).isDirectory();
118586
119238
  } catch {
118587
- refuse4(`There is no folder at ${parent}.`);
119239
+ refuse5(`There is no folder at ${parent}.`);
118588
119240
  }
118589
- if (!dir) refuse4(`${parent} is a file, not a folder.`);
119241
+ if (!dir) refuse5(`${parent} is a file, not a folder.`);
118590
119242
  }
118591
119243
  const target = p.join(parent, name);
118592
119244
  let taken = false;
@@ -118595,7 +119247,7 @@ function projectFolderTarget(spec, home, platform = process.platform) {
118595
119247
  taken = true;
118596
119248
  } catch {
118597
119249
  }
118598
- if (taken) refuse4(`Something named "${name}" is already in ${parent} \u2014 choose another name.`);
119250
+ if (taken) refuse5(`Something named "${name}" is already in ${parent} \u2014 choose another name.`);
118599
119251
  return { name, parent, path: target, isDefault };
118600
119252
  }
118601
119253
  function makeProjectFolder(target) {
@@ -118604,9 +119256,9 @@ function makeProjectFolder(target) {
118604
119256
  mkdirSync16(target.path);
118605
119257
  } catch (e) {
118606
119258
  const code2 = execFailure(e).code;
118607
- if (code2 === "EEXIST") refuse4(`Something named "${target.name}" is already in ${target.parent} \u2014 choose another name.`);
118608
- if (code2 === "ENOENT") refuse4(`There is no folder at ${target.parent}.`);
118609
- refuse4(`Could not make ${target.path}: ${errorMessage(e)}`);
119259
+ if (code2 === "EEXIST") refuse5(`Something named "${target.name}" is already in ${target.parent} \u2014 choose another name.`);
119260
+ if (code2 === "ENOENT") refuse5(`There is no folder at ${target.parent}.`);
119261
+ refuse5(`Could not make ${target.path}: ${errorMessage(e)}`);
118610
119262
  }
118611
119263
  }
118612
119264
 
@@ -118629,7 +119281,7 @@ var ProjectCreation = class {
118629
119281
  const existing = this.projectAt(spelled2);
118630
119282
  const at2 = existing?.path ?? spelled2;
118631
119283
  const id3 = existing?.id ?? randomUUID18();
118632
- this.host.store.projects.addProject({ id: id3, path: at2, name: basename8(at2), ...opts.trusted ? { trusted: true } : {} });
119284
+ this.host.store.projects.addProject({ id: id3, path: at2, name: basename9(at2), ...opts.trusted ? { trusted: true } : {} });
118633
119285
  return this.host.projectInfo(id3, at2);
118634
119286
  }
118635
119287
  /**
@@ -118651,7 +119303,7 @@ var ProjectCreation = class {
118651
119303
  */
118652
119304
  async inspectFolder(given) {
118653
119305
  const typed = given.trim();
118654
- const expanded = /^~(?=$|[\\/])/.test(typed) ? join31(homedir7(), typed.slice(1)) : typed;
119306
+ const expanded = /^~(?=$|[\\/])/.test(typed) ? join32(homedir7(), typed.slice(1)) : typed;
118655
119307
  if (!expanded || !isAbsolute5(expanded)) throw rpcError(`Give the folder's full path: ${JSON.stringify(given)} is not one`);
118656
119308
  const path2 = resolve11(expanded).replace(/^[a-z]:/, (drive) => drive.toUpperCase());
118657
119309
  let found;
@@ -118664,7 +119316,7 @@ var ProjectCreation = class {
118664
119316
  const git2 = folder ? await isRepo(path2).catch(() => false) : false;
118665
119317
  const origin = git2 ? await gitOriginUrl(path2, { trusted: false }) : null;
118666
119318
  const kept = folder ? this.projectAt(path2) : void 0;
118667
- const project2 = kept ? { id: kept.id, name: this.host.store.projects.listProjects().find((p) => p.id === kept.id)?.name ?? basename8(kept.path) } : null;
119319
+ const project2 = kept ? { id: kept.id, name: this.host.store.projects.listProjects().find((p) => p.id === kept.id)?.name ?? basename9(kept.path) } : null;
118668
119320
  return { path: path2, exists: found !== null, folder, git: git2, origin, project: project2 };
118669
119321
  }
118670
119322
  /**
@@ -120459,27 +121111,27 @@ var SessionTools = class {
120459
121111
 
120460
121112
  // packages/agent-host/src/dev-services/handoff-notes.ts
120461
121113
  import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync8 } from "node:fs";
120462
- import { mkdir as mkdir4, readdir as readdir7, writeFile as writeFile4 } from "node:fs/promises";
120463
- import { join as join32 } from "node:path";
120464
- var root = () => join32(dataRoot(), "handoff");
121114
+ import { mkdir as mkdir5, readdir as readdir8, writeFile as writeFile4 } from "node:fs/promises";
121115
+ import { join as join33 } from "node:path";
121116
+ var root = () => join33(dataRoot(), "handoff");
120465
121117
  function handoffNoteDir(projectId) {
120466
121118
  if (!isProjectId(projectId)) {
120467
121119
  throw rpcError(`Not a project id: ${projectId}`);
120468
121120
  }
120469
- return join32(root(), projectId);
121121
+ return join33(root(), projectId);
120470
121122
  }
120471
121123
  function handoffNotePath(projectId, sessionId) {
120472
121124
  if (!isSessionId(sessionId)) {
120473
121125
  throw rpcError(`Not a session id: ${sessionId}`);
120474
121126
  }
120475
- return join32(handoffNoteDir(projectId), `${sessionId}.md`);
121127
+ return join33(handoffNoteDir(projectId), `${sessionId}.md`);
120476
121128
  }
120477
121129
  async function handoffNoteBytes(projectId, sessionId) {
120478
121130
  return dataBytes(handoffNotePath(projectId, sessionId));
120479
121131
  }
120480
121132
  async function writeHandoffNote(projectId, sessionId, text3) {
120481
121133
  const path2 = handoffNotePath(projectId, sessionId);
120482
- await mkdir4(handoffNoteDir(projectId), { recursive: true });
121134
+ await mkdir5(handoffNoteDir(projectId), { recursive: true });
120483
121135
  await removeData(path2);
120484
121136
  await writeFile4(path2, text3, "utf8");
120485
121137
  return path2;
@@ -120494,7 +121146,7 @@ function writeHandoffNoteSync(projectId, sessionId, text3) {
120494
121146
  async function sweepHandoffNotes(owned, projectId) {
120495
121147
  let dirs;
120496
121148
  try {
120497
- dirs = projectId ? [projectId] : (await readdir7(root(), { withFileTypes: true })).filter((d2) => d2.isDirectory()).map((d2) => d2.name);
121149
+ dirs = projectId ? [projectId] : (await readdir8(root(), { withFileTypes: true })).filter((d2) => d2.isDirectory()).map((d2) => d2.name);
120498
121150
  } catch {
120499
121151
  return;
120500
121152
  }
@@ -120505,12 +121157,12 @@ async function sweepHandoffNotes(owned, projectId) {
120505
121157
  } catch {
120506
121158
  continue;
120507
121159
  }
120508
- const entries = await readdir7(dir, { withFileTypes: true }).catch(() => []);
121160
+ const entries = await readdir8(dir, { withFileTypes: true }).catch(() => []);
120509
121161
  for (const e of entries) {
120510
121162
  if (!e.isFile() || !e.name.endsWith(".md")) continue;
120511
121163
  const owner = e.name.slice(0, -".md".length);
120512
121164
  if (owned(owner)) continue;
120513
- await removeData(join32(dir, e.name)).catch(() => {
121165
+ await removeData(join33(dir, e.name)).catch(() => {
120514
121166
  });
120515
121167
  }
120516
121168
  }
@@ -120808,20 +121460,12 @@ var HandoffNotes = class {
120808
121460
  * first, as its children did; the nearest generation with a live one wins.
120809
121461
  */
120810
121462
  handoffHeir(sessionId) {
120811
- const seen = /* @__PURE__ */ new Set([sessionId]);
120812
- let generation = [sessionId];
120813
121463
  try {
120814
- while (generation.length) {
120815
- const heirs = generation.flatMap((at2) => this.host.store.handoffs.handoffHeirs(at2)).filter((h) => !seen.has(h.sessionId));
120816
- for (const h of heirs) seen.add(h.sessionId);
120817
- const live2 = heirs.find((h) => !h.trashed && this.host.meta.has(h.sessionId) && !this.host.trashing.has(h.sessionId));
120818
- if (live2) return live2.sessionId;
120819
- generation = heirs.map((h) => h.sessionId);
120820
- }
121464
+ return liveHeirOf(this.host.store, sessionId, (id3) => this.host.meta.has(id3) && !this.host.trashing.has(id3));
120821
121465
  } catch (e) {
120822
121466
  console.error(`[agent-host] no handoff heir read for ${sessionId.slice(0, 8)}: ${e.message}`);
121467
+ return null;
120823
121468
  }
120824
- return null;
120825
121469
  }
120826
121470
  /**
120827
121471
  * The note folder a successor reads from (#142) — only a project session that inherited a handoff
@@ -120960,11 +121604,11 @@ ${note}` : note;
120960
121604
  };
120961
121605
 
120962
121606
  // packages/agent-host/src/mail/ids.ts
120963
- import { randomBytes as randomBytes7 } from "node:crypto";
121607
+ import { randomBytes as randomBytes8 } from "node:crypto";
120964
121608
  var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
120965
121609
  var ID_RE = /^([a-z0-9][a-z0-9-]*)\.(t|m)_([0-9A-HJKMNP-TV-Z]{26})$/;
120966
121610
  var UlidMaker = class {
120967
- constructor(random = randomBytes7) {
121611
+ constructor(random = randomBytes8) {
120968
121612
  this.random = random;
120969
121613
  }
120970
121614
  random;
@@ -121972,7 +122616,7 @@ function linksOf(raw) {
121972
122616
  function stateOf(word) {
121973
122617
  return THREAD_STATES.find((x) => x === word) ?? "unknown";
121974
122618
  }
121975
- function outcomeOf(word) {
122619
+ function outcomeOf2(word) {
121976
122620
  if (word === null) return null;
121977
122621
  return THREAD_OUTCOMES.find((x) => x === word) ?? "unknown";
121978
122622
  }
@@ -121986,7 +122630,7 @@ var toThread = (r) => ({
121986
122630
  parentId: r.parent_id,
121987
122631
  follows: readJsonList(r.follows, external_exports.string()),
121988
122632
  state: stateOf(r.state),
121989
- outcome: outcomeOf(r.outcome),
122633
+ outcome: outcomeOf2(r.outcome),
121990
122634
  owner: r.owner,
121991
122635
  assignee: r.assignee,
121992
122636
  next: r.next_step,
@@ -123465,165 +124109,6 @@ function attachCommitSessions(commits, records, nameOf3) {
123465
124109
  });
123466
124110
  }
123467
124111
 
123468
- // packages/agent-host/src/mail/rules.ts
123469
- var REFUSALS = [
123470
- "bad_address",
123471
- "not_local",
123472
- "not_found",
123473
- "not_participant",
123474
- "thread_done",
123475
- "kind_not_allowed",
123476
- "field_not_allowed",
123477
- "not_allowed",
123478
- "empty",
123479
- "too_long",
123480
- "too_many_blockers",
123481
- "blockers_stand",
123482
- "no_blocker",
123483
- "bad_follows",
123484
- "no_mailbox",
123485
- "rate_limited",
123486
- "bad_value",
123487
- "policy",
123488
- // mail without threads (docs/plans/mailbox.md §15, S9)
123489
- "gone",
123490
- "ambiguous",
123491
- "unknown",
123492
- "elsewhere",
123493
- "unknown_message",
123494
- "own_message",
123495
- "recipients",
123496
- "not_yours",
123497
- "pair_rate",
123498
- "repeat",
123499
- "ask_rate",
123500
- "orchestrator_asks",
123501
- "removed",
123502
- "untrusted_project"
123503
- ];
123504
- function refuse5(reason, message, more = {}) {
123505
- return rpcError(message, "refused", { retryable: false, data: { reason, ...more } });
123506
- }
123507
- function refusalReason(err) {
123508
- const reason = asObj(asObj(err).data).reason;
123509
- return REFUSALS.find((r) => r === reason);
123510
- }
123511
- function bodyLimit(sender) {
123512
- return sender === "app" ? 4e3 : 16e3;
123513
- }
123514
- function postsPerMinute(sender) {
123515
- return sender === "session" ? 30 : sender === "app" ? 10 : Infinity;
123516
- }
123517
- var ASKS_PER_HOUR = 5;
123518
- var REPEAT_WINDOW_MS = 10 * 6e4;
123519
- function wakes(sender, m) {
123520
- if (m.kind === "event") return false;
123521
- if (sender === "centralu" && m.wake === "no") return false;
123522
- if (sender !== "app") return true;
123523
- if (m.wake === "no") return false;
123524
- if (m.kind === "notice") return m.needsOwner || m.wake === "yes";
123525
- return true;
123526
- }
123527
- function needsReceiver(m) {
123528
- return m.sender === PERSON || m.kind === "task" || m.kind === "report" || m.needsReply || m.facts.needsOwner === true;
123529
- }
123530
- var HOLD_QUESTION_MS = 5 * 6e4;
123531
- var HOLD_OTHER_MS = 15 * 6e4;
123532
- var DEBOUNCE_MS = 2e3;
123533
- function dueAt(now, urgent, fromPerson) {
123534
- return urgent || fromPerson ? now : now + DEBOUNCE_MS;
123535
- }
123536
- var MINUTE_MS = 6e4;
123537
- var HOUR_MS3 = 60 * MINUTE_MS;
123538
- var READ_ITEMS = 10;
123539
- var READ_CHARS = 2e4;
123540
- var READ_CHAIN = 9;
123541
- var PAIR_NEW_PER_HOUR = 5;
123542
- var MAX_RECIPIENTS = 10;
123543
- var HOLD_UNTRUSTED_OUT = true;
123544
- var MAX_DEPTH = 4;
123545
- var HOST_MAIL_TURNS_PER_HOUR = 120;
123546
- var HOST_SHARE = 10;
123547
- var PAIR_TURNS_PER_HOUR = 20;
123548
- var REMINDER = {
123549
- /** After a session came to rest, before the first reminder */
123550
- graceMs: 2 * 6e4,
123551
- /** After a turn the person started */
123552
- personQuietMs: 10 * 6e4,
123553
- /** After the first reminder's turn ended; also the wait after the second before the row is released */
123554
- againMs: 20 * 6e4,
123555
- perRow: 2,
123556
- /** Between two follow-up wakes of one session (reminders and notices to a sender together) */
123557
- gapMs: 20 * 6e4,
123558
- perDay: 6,
123559
- /** A session asleep longer than this since its last turn is not woken for a follow-up (its prompt cache has expired) */
123560
- warmMs: 50 * 6e4,
123561
- /** After delivery a needsReply row is released: the one 2-hour constant, which also bounds a cover (D23, D25) */
123562
- unansweredAfterMs: 2 * HOUR_MS3
123563
- };
123564
- var REFUSAL_TEXT = {
123565
- gone: (address) => `${address} is gone: it is in the session archive or deleted.`,
123566
- no_mailbox: () => "that session cannot read mail: only an ordinary session in a project, or the orchestrator, has the mail tool",
123567
- elsewhere: (address) => `${address} is on another computer, and mail does not reach other computers yet; use agent_session there.`,
123568
- unknown_message: (id3) => `there is no message ${id3} you can read; check the id with mail(action="inbox").`,
123569
- own_message: (id3) => `${id3} is your own message; say who it is for with to="\u2026".`,
123570
- recipients: () => `at most ${MAX_RECIPIENTS} recipients.`,
123571
- not_allowed: () => "that is not a party that takes mail",
123572
- withheld: (id3) => `${id3} is a request: its words are shown in the turn it starts, and it is marked done there.`,
123573
- not_yours: (id3) => `you were not sent ${id3} and did not send it.`,
123574
- pair_rate: (name) => `you started ${PAIR_NEW_PER_HOUR} conversations with ${name} in the last hour; answer one of its messages instead, or wait.`,
123575
- repeat: (name) => `you sent the same text to ${name} less than ${REPEAT_WINDOW_MS / 6e4} minutes ago.`,
123576
- ask_rate: () => `your project's sessions asked the orchestrator ${ASKS_PER_HOUR} times in the last hour; wait, or write to the person.`,
123577
- orchestrator_asks: () => "the orchestrator runs on a tool that cannot ask the person for each step of a request yet, so a session cannot ask it: write to the person instead",
123578
- untrusted_project: (project2, target) => `your project ${project2} is not trusted, so work you give outside it (to ${target}) waits for the person to trust ${project2}. Nothing was started. Centralu shows the person how, and tells you when ${project2} is trusted; give it again then.`
123579
- };
123580
- var ADOPTED_TASK = "Work given before this version of Centralu wrote it down as mail. The conversation of the session that does it shows what was asked.";
123581
- var TASK_CUT = "\n[Centralu: cut here. The full task is in the session's conversation (read_session).]";
123582
- function cutTask(text3, limit) {
123583
- const task = text3.trim();
123584
- return task.length > limit ? task.slice(0, limit - TASK_CUT.length) + TASK_CUT : task;
123585
- }
123586
- function firstLineOf(text3, max = Infinity) {
123587
- return oneLineText(text3.split(/\r\n|[\n\r]/)[0] ?? "", max);
123588
- }
123589
- function wakesMessage(m) {
123590
- return m.facts.quiet !== true;
123591
- }
123592
- function quietAnswer(m, target, receiver) {
123593
- return m.kind === "message" && !m.needsReply && !m.urgent && !receiver.request && target.kind === "message" && !target.needsReply && parseAddress(m.sender)?.kind === "session" && m.sender !== receiver.parent && !receiver.owesTask;
123594
- }
123595
- function depthOf2(cause2, about, recipients) {
123596
- if (!cause2 || cause2.sender === PERSON) return 0;
123597
- const followUp = cause2.kind === "notice" && (cause2.facts.reminder !== void 0 || cause2.facts.unanswered !== void 0);
123598
- if (cause2.kind !== "message" && !followUp) return 0;
123599
- const base2 = followUp ? Math.max(0, ...about.map((m) => m.depth)) : cause2.depth;
123600
- const back = !followUp ? [cause2.sender] : cause2.facts.reminder !== void 0 ? about.map((m) => m.sender) : (cause2.facts.unanswered ?? []).map((u) => u.recipient);
123601
- return recipients.every((r) => back.includes(r)) ? base2 : base2 + 1;
123602
- }
123603
- function pickWork(candidates, now) {
123604
- const classOf = /* @__PURE__ */ new Map();
123605
- for (const c of candidates) {
123606
- const hold = c.needsReply ? HOLD_QUESTION_MS : HOLD_OTHER_MS;
123607
- const k2 = c.decides || now - c.dueAt > hold ? 1 : c.needsReply ? 2 : 3;
123608
- const was = classOf.get(c.key);
123609
- if (was === void 0 || k2 < was) classOf.set(c.key, k2);
123610
- }
123611
- let best = null;
123612
- let bestClass = 4;
123613
- for (const [key, k2] of classOf) {
123614
- if (k2 < bestClass) {
123615
- best = key;
123616
- bestClass = k2;
123617
- }
123618
- }
123619
- return best;
123620
- }
123621
- function flowsDownTo(f) {
123622
- if (f.fromPerson) return true;
123623
- if (!f.fromSession || f.toOrchestrator) return false;
123624
- return f.kind === "task" || f.kind === "message" && f.fromParent;
123625
- }
123626
-
123627
124112
  // packages/agent-host/src/mail/delivery.ts
123628
124113
  var MailNotAtRest = class extends Error {
123629
124114
  constructor() {
@@ -124012,7 +124497,7 @@ var MailDelivery = class {
124012
124497
  */
124013
124498
  overHostCap(recipient, sessionId, drained, now) {
124014
124499
  if (!this.turns) return 0;
124015
- const since = now - HOUR_MS3;
124500
+ const since = now - HOUR_MS;
124016
124501
  if (this.turns.mailStartedSince(since) < this.o.hostCap || this.turns.mailStartedSince(since, sessionId) < this.o.hostShare) return 0;
124017
124502
  if (drained.messageIds.some((id3) => {
124018
124503
  const m = this.records.message(id3);
@@ -124021,9 +124506,9 @@ var MailDelivery = class {
124021
124506
  this.service.notice(PERSON, {
124022
124507
  body: `Mail between sessions started ${this.o.hostCap} turns in the last hour; more wait until the hour has room.`,
124023
124508
  facts: { hostCap: true },
124024
- dedupeKey: `hostcap:${PERSON}:${Math.floor(now / HOUR_MS3) * HOUR_MS3}`
124509
+ dedupeKey: `hostcap:${PERSON}:${Math.floor(now / HOUR_MS) * HOUR_MS}`
124025
124510
  });
124026
- const until = (this.records.oldestMailStartedSince(since, sessionId) ?? now) + HOUR_MS3;
124511
+ const until = (this.records.oldestMailStartedSince(since, sessionId) ?? now) + HOUR_MS;
124027
124512
  if (this.capUntil.get(recipient) !== until) console.error(`[mail] ${recipient}: the host has had ${this.o.hostCap} mail-started turns this hour; its mail waits`);
124028
124513
  this.capUntil.set(recipient, until);
124029
124514
  return Math.max(1e3, until - now);
@@ -125253,16 +125738,16 @@ var CardIds = class {
125253
125738
 
125254
125739
  // packages/agent-host/src/adapters/frames/recorder.ts
125255
125740
  import { appendFileSync as appendFileSync2, existsSync as existsSync16, mkdirSync as mkdirSync18 } from "node:fs";
125256
- import { join as join33 } from "node:path";
125741
+ import { join as join34 } from "node:path";
125257
125742
  var processes = /* @__PURE__ */ new Map();
125258
125743
  function frameRecorder(sessionId, env = process.env) {
125259
125744
  const dir = env.CC_RECORD_FRAMES;
125260
125745
  if (!dir) return void 0;
125261
125746
  mkdirSync18(dir, { recursive: true });
125262
125747
  let n = (processes.get(sessionId) ?? 0) + 1;
125263
- while (existsSync16(join33(dir, `${sessionId}.${n}.jsonl`))) n++;
125748
+ while (existsSync16(join34(dir, `${sessionId}.${n}.jsonl`))) n++;
125264
125749
  processes.set(sessionId, n);
125265
- const file2 = join33(dir, `${sessionId}.${n}.jsonl`);
125750
+ const file2 = join34(dir, `${sessionId}.${n}.jsonl`);
125266
125751
  const start3 = Date.now();
125267
125752
  const write = (entry) => {
125268
125753
  try {
@@ -125711,7 +126196,7 @@ var CoreSession = class {
125711
126196
  };
125712
126197
 
125713
126198
  // packages/agent-host/src/adapters/acp/session.ts
125714
- import { isAbsolute as isAbsolute6, join as join34 } from "node:path";
126199
+ import { isAbsolute as isAbsolute6, join as join35 } from "node:path";
125715
126200
 
125716
126201
  // packages/agent-host/src/adapters/always-allow.ts
125717
126202
  var AlwaysAllowRules = class {
@@ -125853,7 +126338,7 @@ var UnmappedTypes = class {
125853
126338
  };
125854
126339
 
125855
126340
  // packages/agent-host/src/adapters/acp/connection.ts
125856
- import { randomBytes as randomBytes8 } from "node:crypto";
126341
+ import { randomBytes as randomBytes9 } from "node:crypto";
125857
126342
  var RpcError = class extends Error {
125858
126343
  constructor(code2, message, data) {
125859
126344
  super(message);
@@ -125898,7 +126383,7 @@ var AcpConnection = class {
125898
126383
  * String ids with a per-connection prefix: a host that adopts a process the previous host was talking to can
125899
126384
  * receive answers to that host's requests (§4.4), and plain counters on both would have collided.
125900
126385
  */
125901
- idPrefix = `c${randomBytes8(3).toString("hex")}-`;
126386
+ idPrefix = `c${randomBytes9(3).toString("hex")}-`;
125902
126387
  nextId = 1;
125903
126388
  pending = new PendingRequests();
125904
126389
  closed = false;
@@ -127108,7 +127593,7 @@ var AcpWire = class {
127108
127593
  ownTool: Boolean(this.deps.centraluToolOf?.(call2)),
127109
127594
  auto: this.preset === "auto",
127110
127595
  alwaysAllowKey: alwaysAllowKey(detail),
127111
- edit: edit ? { folder: this.opts.cwd, paths: editPaths(call2).map((p) => isAbsolute6(p) ? p : join34(this.opts.cwd, p)) } : null
127596
+ edit: edit ? { folder: this.opts.cwd, paths: editPaths(call2).map((p) => isAbsolute6(p) ? p : join35(this.opts.cwd, p)) } : null
127112
127597
  };
127113
127598
  }
127114
127599
  respondApproval(requestId, decision, matcher2) {
@@ -127265,10 +127750,10 @@ var ACP_CAPABILITIES = {
127265
127750
  // packages/agent-host/src/adapters/antigravity/agent-files.ts
127266
127751
  import { spawn as spawnChild } from "node:child_process";
127267
127752
  import { createHash as createHash11 } from "node:crypto";
127268
- import { closeSync as closeSync5, createReadStream as createReadStream3, createWriteStream, existsSync as existsSync18, fstatSync as fstatSync4, openSync as openSync5, readdirSync as readdirSync11, readFileSync as readFileSync12, readSync as readSync4, watch as watch3 } from "node:fs";
127269
- import { chmod as chmod3, mkdir as mkdir5, mkdtemp as mkdtemp3, readdir as readdir8, readFile as readFile4, rename as rename4, rm as rm6, statfs, writeFile as writeFile5 } from "node:fs/promises";
127753
+ import { closeSync as closeSync5, createReadStream as createReadStream3, createWriteStream, existsSync as existsSync18, fstatSync as fstatSync4, openSync as openSync5, readdirSync as readdirSync11, readFileSync as readFileSync12, readSync as readSync4, watch as watch4 } from "node:fs";
127754
+ import { chmod as chmod3, mkdir as mkdir6, mkdtemp as mkdtemp3, readdir as readdir9, readFile as readFile5, rename as rename5, rm as rm6, statfs, writeFile as writeFile5 } from "node:fs/promises";
127270
127755
  import { homedir as homedir8 } from "node:os";
127271
- import { join as join36 } from "node:path";
127756
+ import { join as join37 } from "node:path";
127272
127757
  import { Transform } from "node:stream";
127273
127758
  import { pipeline } from "node:stream/promises";
127274
127759
  import { createInflateRaw } from "node:zlib";
@@ -127276,7 +127761,7 @@ import { createInflateRaw } from "node:zlib";
127276
127761
  // packages/agent-host/src/dev-services/instance-lock.ts
127277
127762
  import { execFileSync as execFileSync3 } from "node:child_process";
127278
127763
  import { readFileSync as readFileSync11, unlinkSync as unlinkSync3, writeFileSync as writeFileSync9 } from "node:fs";
127279
- import { dirname as dirname16, join as join35 } from "node:path";
127764
+ import { dirname as dirname16, join as join36 } from "node:path";
127280
127765
  import Database2 from "better-sqlite3";
127281
127766
  var OWNERSHIP_FILE = "host-ownership.sqlite";
127282
127767
  var held = /* @__PURE__ */ new Set();
@@ -127317,7 +127802,7 @@ function parseHolder(raw) {
127317
127802
  function acquireInstanceLock(dbPath2, startOf = processStartTime, describe4 = {}, now = Date.now) {
127318
127803
  if (dbPath2 === ":memory:") return { ok: true, release: () => {
127319
127804
  } };
127320
- const lockPath = join35(dirname16(dbPath2), "host.lock");
127805
+ const lockPath = join36(dirname16(dbPath2), "host.lock");
127321
127806
  const readHolder = () => {
127322
127807
  try {
127323
127808
  return parseHolder(readFileSync11(lockPath, "utf8"));
@@ -127325,7 +127810,7 @@ function acquireInstanceLock(dbPath2, startOf = processStartTime, describe4 = {}
127325
127810
  return null;
127326
127811
  }
127327
127812
  };
127328
- const owner = takeOwnership(join35(dirname16(dbPath2), OWNERSHIP_FILE));
127813
+ const owner = takeOwnership(join36(dirname16(dbPath2), OWNERSHIP_FILE));
127329
127814
  if (!owner) {
127330
127815
  const holder = readHolder();
127331
127816
  const pid = holder && Number.isInteger(holder.pid) && holder.pid > 0 ? holder.pid : null;
@@ -127450,26 +127935,26 @@ function agyPlatformKey(platform = HOST_PLATFORM, arch = process.arch) {
127450
127935
  return null;
127451
127936
  }
127452
127937
  function agyHome(dataDir, version2) {
127453
- const root3 = join36(dataDir, "tools", "antigravity");
127454
- return { root: root3, versionDir: join36(root3, version2), geminiHome: join36(root3, "gemini-home") };
127938
+ const root3 = join37(dataDir, "tools", "antigravity");
127939
+ return { root: root3, versionDir: join37(root3, version2), geminiHome: join37(root3, "gemini-home") };
127455
127940
  }
127456
127941
  function agyProgram(home, archive) {
127457
- return join36(home.versionDir, archive.command);
127942
+ return join37(home.versionDir, archive.command);
127458
127943
  }
127459
127944
  var INSTALL_MARKER = "install.json";
127460
127945
  function isInstalled(home, archive) {
127461
127946
  try {
127462
- const marker = asObj(JSON.parse(readFileSync12(join36(home.versionDir, INSTALL_MARKER), "utf8")));
127947
+ const marker = asObj(JSON.parse(readFileSync12(join37(home.versionDir, INSTALL_MARKER), "utf8")));
127463
127948
  if (marker.sha256 !== archive.sha256) return false;
127464
- return archive.files.every((f) => existsSync18(join36(home.versionDir, f)));
127949
+ return archive.files.every((f) => existsSync18(join37(home.versionDir, f)));
127465
127950
  } catch {
127466
127951
  return false;
127467
127952
  }
127468
127953
  }
127469
127954
  function hasSignIn(home) {
127470
- const dir = join36(home.geminiHome, "antigravity-acp");
127955
+ const dir = join37(home.geminiHome, "antigravity-acp");
127471
127956
  try {
127472
- const settings = asObj(JSON.parse(readFileSync12(join36(dir, "settings.json"), "utf8")));
127957
+ const settings = asObj(JSON.parse(readFileSync12(join37(dir, "settings.json"), "utf8")));
127473
127958
  const auth = asObj(settings.auth);
127474
127959
  return typeof auth.type === "string" && auth.type.length > 0;
127475
127960
  } catch {
@@ -127529,22 +128014,22 @@ async function installAgy(home, archive, version2, opts) {
127529
128014
  const io2 = opts.io ?? realInstallIo;
127530
128015
  const { signal } = opts;
127531
128016
  if (isInstalled(home, archive)) return;
127532
- await mkdir5(home.root, { recursive: true });
128017
+ await mkdir6(home.root, { recursive: true });
127533
128018
  await removeStale(home.root, ".install-");
127534
128019
  const free = await io2.freeBytes(home.root);
127535
128020
  if (free !== null && free < AGY_INSTALL_SPACE_BYTES) {
127536
128021
  throw new InstallFailure("disk_space", `Antigravity needs about ${mb(AGY_INSTALL_SPACE_BYTES)} MB free to install; ${mb(free)} MB are free`);
127537
128022
  }
127538
- const work = await mkdtemp3(join36(home.root, `.install-${ownerTag()}-`));
128023
+ const work = await mkdtemp3(join37(home.root, `.install-${ownerTag()}-`));
127539
128024
  try {
127540
- const zip = join36(work, "archive.zip");
128025
+ const zip = join37(work, "archive.zip");
127541
128026
  const sha = await download(io2, archive.url, zip, signal, opts.onProgress);
127542
128027
  if (sha !== archive.sha256) throw new InstallFailure("checksum", `The downloaded archive does not match the version Centralu pins (sha256 ${sha.slice(0, 12)}\u2026)`);
127543
128028
  cancelled(signal);
127544
- const pkg = join36(work, "pkg");
128029
+ const pkg = join37(work, "pkg");
127545
128030
  await unpack(zip, archive, pkg, signal, opts.platform ?? HOST_PLATFORM);
127546
128031
  await rm6(zip, { force: true });
127547
- await writeFile5(join36(pkg, INSTALL_MARKER), `${JSON.stringify({ version: version2, sha256: archive.sha256, url: archive.url })}
128032
+ await writeFile5(join37(pkg, INSTALL_MARKER), `${JSON.stringify({ version: version2, sha256: archive.sha256, url: archive.url })}
127548
128033
  `);
127549
128034
  cancelled(signal);
127550
128035
  if (isInstalled(home, archive)) return;
@@ -127607,12 +128092,12 @@ async function unpack(zip, archive, dir, signal, platform) {
127607
128092
  if (names.length !== want.length || names.some((n, i) => n !== want[i])) {
127608
128093
  throw new InstallFailure("unpack", `The archive holds ${names.join(", ") || "nothing"}, not the files the release names (${want.join(", ")})`);
127609
128094
  }
127610
- await mkdir5(dir, { recursive: true });
128095
+ await mkdir6(dir, { recursive: true });
127611
128096
  for (const e of entries) {
127612
128097
  if (e.kind !== "file" || !/^[\w.-]+$/.test(e.name)) throw new InstallFailure("unpack", `The archive's entry ${e.name} is not a plain file`);
127613
128098
  if (e.method !== 0 && e.method !== 8) throw new InstallFailure("unpack", `The archive's entry ${e.name} uses compression method ${e.method}`);
127614
128099
  cancelled(signal);
127615
- const path2 = join36(dir, e.name);
128100
+ const path2 = join37(dir, e.name);
127616
128101
  await unpackEntry(zip, e, starts.get(e) ?? 0, path2, signal);
127617
128102
  if (platform !== "win32") await chmod3(path2, 493);
127618
128103
  }
@@ -127662,9 +128147,9 @@ async function removeLeftovers(home) {
127662
128147
  await removeStale(home.root, ".sign-in-");
127663
128148
  }
127664
128149
  async function removeStale(root3, prefix) {
127665
- const names = await readdir8(root3).catch(() => []);
128150
+ const names = await readdir9(root3).catch(() => []);
127666
128151
  const stale = names.filter((n) => n.startsWith(prefix) && ownerGone(n.slice(prefix.length)));
127667
- await Promise.all(stale.map((n) => rm6(join36(root3, n), { recursive: true, force: true }).catch(() => {
128152
+ await Promise.all(stale.map((n) => rm6(join37(root3, n), { recursive: true, force: true }).catch(() => {
127668
128153
  })));
127669
128154
  }
127670
128155
  var ownStart;
@@ -127695,10 +128180,10 @@ function processAlive(pid) {
127695
128180
  }
127696
128181
  }
127697
128182
  async function removeOtherVersions(home) {
127698
- const names = await readdir8(home.root).catch(() => []);
128183
+ const names = await readdir9(home.root).catch(() => []);
127699
128184
  for (const n of names) {
127700
128185
  if (!/^\d+\.\d+\.\d+/.test(n)) continue;
127701
- const dir = join36(home.root, n);
128186
+ const dir = join37(home.root, n);
127702
128187
  if (dir === home.versionDir) continue;
127703
128188
  await rm6(dir, { recursive: true, force: true }).catch(() => {
127704
128189
  });
@@ -127707,7 +128192,7 @@ async function removeOtherVersions(home) {
127707
128192
  async function renameHeld(from, to) {
127708
128193
  for (let attempt = 1; ; attempt++) {
127709
128194
  try {
127710
- await rename4(from, to);
128195
+ await rename5(from, to);
127711
128196
  return;
127712
128197
  } catch (err) {
127713
128198
  const code2 = err instanceof Error ? Reflect.get(err, "code") : void 0;
@@ -127718,21 +128203,21 @@ async function renameHeld(from, to) {
127718
128203
  }
127719
128204
  var mb = (bytes2) => Math.round(bytes2 / (1024 * 1024));
127720
128205
  async function browserFolder(home, platform = HOST_PLATFORM, node = process.execPath) {
127721
- await mkdir5(home.root, { recursive: true });
128206
+ await mkdir6(home.root, { recursive: true });
127722
128207
  await removeStale(home.root, ".sign-in-");
127723
- const dir = await mkdtemp3(join36(home.root, `.sign-in-${ownerTag()}-`));
128208
+ const dir = await mkdtemp3(join37(home.root, `.sign-in-${ownerTag()}-`));
127724
128209
  await chmod3(dir, 448).catch(() => {
127725
128210
  });
127726
128211
  let browser;
127727
128212
  if (platform === "win32") {
127728
- const script = join36(dir, "open-url.mjs");
128213
+ const script = join37(dir, "open-url.mjs");
127729
128214
  await writeFile5(script, WINDOWS_HELPER);
127730
128215
  browser = windowsBrowserCommand(node, script);
127731
128216
  } else {
127732
- const helper = join36(dir, "open-url");
127733
- const part = `${shQuote(join36(dir, "part-"))}"$$"`;
128217
+ const helper = join37(dir, "open-url");
128218
+ const part = `${shQuote(join37(dir, "part-"))}"$$"`;
127734
128219
  await writeFile5(helper, `#!/bin/sh
127735
- printf '%s\\n' "$1" > ${part} && mv ${part} ${shQuote(join36(dir, "url-"))}"$$"
128220
+ printf '%s\\n' "$1" > ${part} && mv ${part} ${shQuote(join37(dir, "url-"))}"$$"
127736
128221
  exit 0
127737
128222
  `, { mode: 448 });
127738
128223
  await chmod3(helper, 448);
@@ -127752,8 +128237,8 @@ exit 0
127752
128237
  }
127753
128238
  for (const name of names) {
127754
128239
  taken.add(name);
127755
- const path2 = join36(dir, name);
127756
- void readFile4(path2, "utf8").then(
128240
+ const path2 = join37(dir, name);
128241
+ void readFile5(path2, "utf8").then(
127757
128242
  async (text3) => {
127758
128243
  await rm6(path2, { force: true }).catch(() => {
127759
128244
  });
@@ -127765,7 +128250,7 @@ exit 0
127765
128250
  }
127766
128251
  };
127767
128252
  try {
127768
- watcher = watch3(dir, () => look());
128253
+ watcher = watch4(dir, () => look());
127769
128254
  watcher.on("error", () => {
127770
128255
  });
127771
128256
  } catch {
@@ -128295,10 +128780,10 @@ var AntigravityAdapter = class extends AcpAdapter {
128295
128780
 
128296
128781
  // packages/agent-host/src/adapters/claude/index.ts
128297
128782
  import { tmpdir as tmpdir4 } from "node:os";
128298
- import { mkdtemp as mkdtemp4, readdir as readdir11, rm as rm9, stat as stat10 } from "node:fs/promises";
128299
- import { join as join44 } from "node:path";
128783
+ import { mkdtemp as mkdtemp4, readdir as readdir12, rm as rm9, stat as stat11 } from "node:fs/promises";
128784
+ import { join as join45 } from "node:path";
128300
128785
  import { randomUUID as randomUUID23 } from "node:crypto";
128301
- import { execFile as execFile5 } from "node:child_process";
128786
+ import { execFile as execFile6 } from "node:child_process";
128302
128787
  import { promisify as promisify4 } from "node:util";
128303
128788
 
128304
128789
  // packages/agent-host/src/cli-version.ts
@@ -128651,13 +129136,28 @@ function claudeQueryOptions({ sessionId, opts, executablePath, servers, permissi
128651
129136
  }
128652
129137
 
128653
129138
  // packages/agent-host/src/adapters/claude/exe-link.ts
128654
- import { randomBytes as randomBytes9 } from "node:crypto";
129139
+ import { execFile as execFile5 } from "node:child_process";
129140
+ import { randomBytes as randomBytes10 } from "node:crypto";
128655
129141
  import { closeSync as closeSync7, copyFileSync as copyFileSync4, linkSync as linkSync2, mkdirSync as mkdirSync20, openSync as openSync7, readdirSync as readdirSync13, readFileSync as readFileSync15, readSync as readSync6, renameSync as renameSync11, rmSync as rmSync6, statSync as statSync15 } from "node:fs";
128656
- import { basename as basename11, dirname as dirname20, join as join41 } from "node:path";
129142
+ import { createRequire } from "node:module";
129143
+ import { basename as basename12, dirname as dirname20, join as join42 } from "node:path";
128657
129144
  var PLACEHOLDER_MAX_BYTES = 1e6;
129145
+ var INSTALLER_TIMEOUT_MS = 3e4;
129146
+ var VERSION_TIMEOUT_MS = 1e4;
128658
129147
  var ClaudePlaceholderError = class extends Error {
128659
129148
  name = "ClaudePlaceholderError";
128660
129149
  };
129150
+ function runNode(file2, args, cwd, timeoutMs) {
129151
+ return new Promise((resolve20) => {
129152
+ const child = execFile5(file2, args, { cwd, timeout: timeoutMs, windowsHide: true, maxBuffer: 1 << 20, encoding: "utf8" }, (err, stdout, stderr) => {
129153
+ const output = `${stdout}${stderr}`.trim();
129154
+ if (!err) return resolve20({ status: 0, output });
129155
+ if (err.killed) return resolve20({ status: null, output: `it did not finish within ${Math.round(timeoutMs / 1e3)} seconds` });
129156
+ resolve20(typeof err.code === "number" ? { status: err.code, output } : { status: null, output: err.message });
129157
+ });
129158
+ child.stdin?.end();
129159
+ });
129160
+ }
128661
129161
  var realExeFs = {
128662
129162
  stat: (p) => statSync15(p),
128663
129163
  head: (p, n) => {
@@ -128677,9 +129177,33 @@ var realExeFs = {
128677
129177
  list: (d2) => readdirSync13(d2),
128678
129178
  mkdir: (d2) => void mkdirSync20(d2, { recursive: true }),
128679
129179
  removeDir: (d2) => rmSync6(d2, { recursive: true, force: true }),
128680
- removeFile: (p) => rmSync6(p, { force: true })
129180
+ removeFile: (p) => rmSync6(p, { force: true }),
129181
+ runInstaller: (installer, timeoutMs) => runNode(process.execPath, [installer], dirname20(installer), timeoutMs),
129182
+ /*
129183
+ * Where `install.cjs` takes the program from: the platform package its `package.json` lists, resolved
129184
+ * from the package's own folder. Only Windows repairs, so the platform is win32 and the file claude.exe.
129185
+ */
129186
+ nativeSize: (installer) => {
129187
+ try {
129188
+ const pkg = createRequire(installer).resolve(`@anthropic-ai/claude-code-win32-${process.arch}/package.json`);
129189
+ return statSync15(join42(dirname20(pkg), "claude.exe")).size;
129190
+ } catch {
129191
+ return null;
129192
+ }
129193
+ },
129194
+ runProgram: async (path2, args, timeoutMs) => ({ status: (await runNode(path2, args, void 0, timeoutMs)).status })
128681
129195
  };
128682
- function placeholderProblem(exe, size2, fs3 = realExeFs) {
129196
+ function installerFor(exe, fs3 = realExeFs) {
129197
+ if (basename12(dirname20(exe)).toLowerCase() !== "bin") return null;
129198
+ const installer = join42(dirname20(dirname20(exe)), "install.cjs");
129199
+ try {
129200
+ fs3.stat(installer);
129201
+ return installer;
129202
+ } catch {
129203
+ return null;
129204
+ }
129205
+ }
129206
+ function placeholderProblem(exe, size2, fs3 = realExeFs, tried = null) {
128683
129207
  if (size2 >= PLACEHOLDER_MAX_BYTES) return null;
128684
129208
  let head;
128685
129209
  try {
@@ -128688,26 +129212,23 @@ function placeholderProblem(exe, size2, fs3 = realExeFs) {
128688
129212
  return null;
128689
129213
  }
128690
129214
  if (head.toString("latin1") === "MZ") return null;
128691
- const pkg = dirname20(dirname20(exe));
128692
- const installer = basename11(dirname20(exe)).toLowerCase() === "bin" ? join41(pkg, "install.cjs") : null;
128693
- let canRerun = false;
128694
- if (installer) {
128695
- try {
128696
- fs3.stat(installer);
128697
- canRerun = true;
128698
- } catch {
128699
- }
128700
- }
128701
129215
  return [
128702
129216
  `Claude Code is not installed completely: ${exe} is a ${size2}-byte placeholder, not the Claude program (about 250 MB).`,
128703
129217
  "Claude Code's setup step puts the real program there after npm installs the package; it did not finish, usually because a running Claude Code held the file during an install or update.",
128704
- canRerun ? `To fix it, close every Claude Code (Centralu's Claude sessions, terminals, editors) and run \`node "${installer}"\`, or reinstall with \`npm i -g @anthropic-ai/claude-code\` while no Claude Code is running.` : "To fix it, close every Claude Code (Centralu's Claude sessions, terminals, editors) and reinstall it with `npm i -g @anthropic-ai/claude-code`."
129218
+ ...tried ? [tried] : [],
129219
+ fixAdvice(installerFor(exe, fs3))
128705
129220
  ].join(" ");
128706
129221
  }
129222
+ function fixAdvice(installer) {
129223
+ return installer ? `To fix it, close every Claude Code (Centralu's Claude sessions, terminals, editors) and run \`node "${installer}"\`, or reinstall with \`npm i -g @anthropic-ai/claude-code\` while no Claude Code is running.` : "To fix it, close every Claude Code (Centralu's Claude sessions, terminals, editors) and reinstall it with `npm i -g @anthropic-ai/claude-code`.";
129224
+ }
129225
+ function unprovenProblem(exe, size2, why2, tried, installer) {
129226
+ return [`Claude Code is not installed completely: ${exe} is ${size2} bytes and ${why2}.`, tried, fixAdvice(installer)].join(" ");
129227
+ }
128707
129228
  function claudeKey(exe, stat12, fs3 = realExeFs) {
128708
129229
  let version2 = null;
128709
129230
  try {
128710
- const pkg = JSON.parse(fs3.read(join41(dirname20(dirname20(exe)), "package.json")));
129231
+ const pkg = JSON.parse(fs3.read(join42(dirname20(dirname20(exe)), "package.json")));
128711
129232
  if (pkg.name === "@anthropic-ai/claude-code" && typeof pkg.version === "string" && pkg.version) version2 = pkg.version;
128712
129233
  } catch {
128713
129234
  }
@@ -128727,19 +129248,31 @@ var ClaudeLinks = class {
128727
129248
  fs;
128728
129249
  log;
128729
129250
  uses = /* @__PURE__ */ new Map();
129251
+ /** The setup step of each package this host has started, running or done: once per host start, never in a loop */
129252
+ repairs = /* @__PURE__ */ new Map();
129253
+ /** What the setup step did, once it ended: what the message adds about it, and the program it left that is not proven */
129254
+ outcomes = /* @__PURE__ */ new Map();
128730
129255
  /** The key new sessions start from: the last one prepared */
128731
129256
  current = null;
128732
129257
  get enabled() {
128733
129258
  return windowsStart(this.opts.platform);
128734
129259
  }
128735
129260
  base() {
128736
- return join41(this.opts.root(), "tools", "claude");
129261
+ return join42(this.opts.root(), "tools", "claude");
129262
+ }
129263
+ /**
129264
+ * `prepare`, after repairing npm's placeholder if that is what `exe` is. Every start path awaits this,
129265
+ * so the setup step runs beside the host's loop, never in it.
129266
+ */
129267
+ async ensure(exe) {
129268
+ await this.repair(exe);
129269
+ return this.prepare(exe);
128737
129270
  }
128738
129271
  /**
128739
129272
  * What to start for the `claude.exe` found at `exe`. Off Windows, or for anything but an `.exe`
128740
129273
  * (a `cli.js` through Node), `exe` itself.
128741
129274
  *
128742
- * @throws ClaudePlaceholderError when the file is npm's placeholder
129275
+ * @throws ClaudePlaceholderError when the file is npm's placeholder, or a setup step left a file that is not proven to be the program
128743
129276
  */
128744
129277
  prepare(exe) {
128745
129278
  const direct = { path: exe, key: null };
@@ -128750,11 +129283,15 @@ var ClaudeLinks = class {
128750
129283
  } catch {
128751
129284
  return direct;
128752
129285
  }
128753
- const problem3 = placeholderProblem(exe, stat12.size, this.fs);
129286
+ const installer = installerFor(exe, this.fs);
129287
+ const outcome = installer ? this.outcomes.get(installer) : void 0;
129288
+ let problem3 = placeholderProblem(exe, stat12.size, this.fs, outcome?.note ?? null);
129289
+ const bad = outcome?.unproven;
129290
+ if (!problem3 && installer && outcome && bad && bad.size === stat12.size && bad.mtimeMs === stat12.mtimeMs) problem3 = unprovenProblem(exe, stat12.size, bad.why, outcome.note, installer);
128754
129291
  if (problem3) throw new ClaudePlaceholderError(problem3);
128755
129292
  const key = claudeKey(exe, stat12, this.fs);
128756
- const dir = join41(this.base(), key);
128757
- const target = join41(dir, "claude.exe");
129293
+ const dir = join42(this.base(), key);
129294
+ const target = join42(dir, "claude.exe");
128758
129295
  const ready = () => {
128759
129296
  try {
128760
129297
  return this.fs.stat(target).size === stat12.size;
@@ -128763,7 +129300,7 @@ var ClaudeLinks = class {
128763
129300
  }
128764
129301
  };
128765
129302
  if (ready()) return this.use(key, target);
128766
- const tmp = join41(dir, `claude-${randomBytes9(4).toString("hex")}.tmp`);
129303
+ const tmp = join42(dir, `claude-${randomBytes10(4).toString("hex")}.tmp`);
128767
129304
  try {
128768
129305
  this.fs.mkdir(dir);
128769
129306
  try {
@@ -128788,6 +129325,74 @@ var ClaudeLinks = class {
128788
129325
  return direct;
128789
129326
  }
128790
129327
  }
129328
+ /**
129329
+ * Runs the package's own setup step, once per package per host start, when `exe` is npm's placeholder
129330
+ * and `install.cjs` is beside it (the step that was meant to put the program there; nothing else runs
129331
+ * and nothing is fetched). Starts that arrive meanwhile wait for the same run. Never stops a process:
129332
+ * a file a running Claude Code holds stays a placeholder and the message says so. Never rejects.
129333
+ */
129334
+ repair(exe) {
129335
+ if (!this.enabled || !/\.exe$/i.test(exe)) return Promise.resolve();
129336
+ let size2;
129337
+ try {
129338
+ size2 = this.fs.stat(exe).size;
129339
+ } catch {
129340
+ return Promise.resolve();
129341
+ }
129342
+ if (placeholderProblem(exe, size2, this.fs) === null) return Promise.resolve();
129343
+ const installer = installerFor(exe, this.fs);
129344
+ if (!installer) return Promise.resolve();
129345
+ let run4 = this.repairs.get(installer);
129346
+ if (!run4) {
129347
+ run4 = this.runSetupStep(exe, installer).catch((err) => {
129348
+ this.log(`[claude] the setup step could not be run: ${code(err)}`);
129349
+ this.outcomes.set(installer, { note: `Centralu tried to run that setup step (\`node "${installer}"\`) and could not: ${code(err)}.`, unproven: null });
129350
+ });
129351
+ this.repairs.set(installer, run4);
129352
+ }
129353
+ return run4;
129354
+ }
129355
+ async runSetupStep(exe, installer) {
129356
+ this.log(`[claude] ${exe} is npm's placeholder; running Claude Code's own setup step, ${installer}`);
129357
+ const run4 = await this.fs.runInstaller(installer, this.opts.installerTimeoutMs ?? INSTALLER_TIMEOUT_MS);
129358
+ const step = `Centralu ran that setup step itself (\`node "${installer}"\`)`;
129359
+ const verdict = await this.proven(exe, installer);
129360
+ if (verdict.ok) {
129361
+ this.log(`[claude] repaired ${exe}`);
129362
+ this.outcomes.set(installer, { note: `Centralu already ran that setup step once since it started (\`node "${installer}"\`) and does not run it again.`, unproven: null });
129363
+ return;
129364
+ }
129365
+ const ran = run4.status === null ? run4.output : run4.status === 0 ? "" : `exit code ${run4.status}: ${firstLine3(run4.output)}`;
129366
+ if (verdict.stat) {
129367
+ this.log(`[claude] the setup step left ${verdict.stat.size} bytes at ${exe} that are not proven to be the program (${verdict.why})${ran ? `; ${ran}` : ""}`);
129368
+ this.outcomes.set(installer, { note: `${step} and it did not finish the copy${ran ? `: ${ran}` : ""}.`, unproven: { ...verdict.stat, why: verdict.why } });
129369
+ } else if (/\b(EBUSY|EPERM|EACCES)\b/.test(run4.output)) {
129370
+ this.log(`[claude] the setup step could not replace ${exe}; a running program holds it: ${firstLine3(run4.output)}`);
129371
+ this.outcomes.set(installer, { note: `${step} and the file is still a placeholder: it could not be replaced, usually because a running Claude Code held it.`, unproven: null });
129372
+ } else {
129373
+ const why2 = ran || firstLine3(run4.output) || "it left the placeholder in place";
129374
+ this.log(`[claude] the setup step did not repair ${exe}: ${why2}`);
129375
+ this.outcomes.set(installer, { note: `${step} and it did not repair the file: ${why2}.`, unproven: null });
129376
+ }
129377
+ }
129378
+ /**
129379
+ * Whether the file at `exe` is the program. Not "large enough": a copy cut by the time limit is large.
129380
+ * It must be the size of the platform package's program it was made from; where that cannot be found,
129381
+ * it must run `--version` and exit 0.
129382
+ */
129383
+ async proven(exe, installer) {
129384
+ let stat12;
129385
+ try {
129386
+ stat12 = this.fs.stat(exe);
129387
+ } catch {
129388
+ return { ok: false, why: "the file is gone" };
129389
+ }
129390
+ if (placeholderProblem(exe, stat12.size, this.fs) !== null) return { ok: false, why: "it is still the placeholder" };
129391
+ const native = this.fs.nativeSize(installer);
129392
+ if (native !== null) return stat12.size === native ? { ok: true } : { ok: false, stat: stat12, why: `not the ${native} bytes of the Claude program it was copied from` };
129393
+ const version2 = await this.fs.runProgram(exe, ["--version"], VERSION_TIMEOUT_MS);
129394
+ return version2.status === 0 ? { ok: true } : { ok: false, stat: stat12, why: "it does not run (`--version` failed)" };
129395
+ }
128791
129396
  use(key, path2) {
128792
129397
  if (key !== this.current) {
128793
129398
  this.current = key;
@@ -128823,7 +129428,7 @@ var ClaudeLinks = class {
128823
129428
  for (const name of names) {
128824
129429
  if (name === this.current || this.uses.has(name)) continue;
128825
129430
  try {
128826
- this.fs.removeDir(join41(this.base(), name));
129431
+ this.fs.removeDir(join42(this.base(), name));
128827
129432
  } catch {
128828
129433
  }
128829
129434
  }
@@ -128837,6 +129442,10 @@ var ClaudeLinks = class {
128837
129442
  }
128838
129443
  }
128839
129444
  };
129445
+ function firstLine3(text3) {
129446
+ const line = text3.split(/\r?\n/).find((l) => l.trim())?.trim() ?? "";
129447
+ return line.length > 240 ? `${line.slice(0, 240)}...` : line;
129448
+ }
128840
129449
  function code(err) {
128841
129450
  return err?.code ?? err?.message ?? String(err);
128842
129451
  }
@@ -128960,7 +129569,7 @@ function contentBlocks(m) {
128960
129569
  }
128961
129570
 
128962
129571
  // packages/agent-host/src/adapters/images.ts
128963
- import { readFile as readFile6, stat as stat8 } from "node:fs/promises";
129572
+ import { readFile as readFile7, stat as stat9 } from "node:fs/promises";
128964
129573
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "webp", "bmp"]);
128965
129574
  var startsWith = (buf, bytes2, at2 = 0) => bytes2.every((b2, i) => buf[at2 + i] === b2);
128966
129575
  function gifReachesTrailer(buf) {
@@ -129041,9 +129650,9 @@ async function imageEventFromDisk(sessionId, path2, maxBytes = IMAGE_MAX_BYTES)
129041
129650
  const file2 = path2.split(/[\\/]/).pop() ?? path2;
129042
129651
  const ext2 = file2.includes(".") ? file2.slice(file2.lastIndexOf(".") + 1).toLowerCase() : "";
129043
129652
  try {
129044
- const s = await stat8(path2);
129653
+ const s = await stat9(path2);
129045
129654
  if (s.size > maxBytes) return fail4(tooLarge(s.size));
129046
- const buf = await readFile6(path2);
129655
+ const buf = await readFile7(path2);
129047
129656
  const seen = judgeImage(buf);
129048
129657
  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`);
129049
129658
  if ("note" in seen) return fail4(seen.note);
@@ -129060,12 +129669,13 @@ function commandRunOf(name, input) {
129060
129669
  const run4 = centraluRunCommand(input);
129061
129670
  return run4 ? { name: name.slice(0, -"centralu_run".length) + run4.command, input: run4.args } : null;
129062
129671
  }
129672
+ var SHELL_TOOLS = /* @__PURE__ */ new Set(["Bash", "PowerShell"]);
129063
129673
  function toolSummary(name, input) {
129064
129674
  const run4 = commandRunOf(name, input);
129065
129675
  if (run4) return toolSummary(run4.name, run4.input);
129066
129676
  const paths = [];
129067
129677
  let title = name;
129068
- if (name === "Bash") title = str(input.command, name);
129678
+ if (SHELL_TOOLS.has(name)) title = str(input.command, name);
129069
129679
  else if (FILE_EDIT_TOOLS.has(name) || name === "Read") {
129070
129680
  const p = str(input.file_path ?? input.notebook_path);
129071
129681
  if (p) paths.push(p);
@@ -129120,7 +129730,7 @@ ${content.replace(/\n$/, "")}` : "" };
129120
129730
  ${diff}` : diff };
129121
129731
  }
129122
129732
  function approvalDetail(name, input, cwd, isNew) {
129123
- if (name === "Bash") return { kind: "command", command: str(input.command), cwd, ...input.dangerouslyDisableSandbox === true ? { unsandboxed: true } : {} };
129733
+ if (SHELL_TOOLS.has(name)) return { kind: "command", command: str(input.command), cwd, ...input.dangerouslyDisableSandbox === true ? { unsandboxed: true } : {} };
129124
129734
  if (FILE_EDIT_TOOLS.has(name)) {
129125
129735
  const change = claudeFileChange(name, input, isNew);
129126
129736
  return fileEditDetail([change.diff ? change : { ...change, diff: JSON.stringify(input) }]);
@@ -130452,16 +131062,16 @@ var ClaudeStreamNormalizer = class {
130452
131062
 
130453
131063
  // packages/agent-host/src/adapters/claude/lsp-plugins.ts
130454
131064
  init_sdk();
130455
- import { access as access3, readFile as readFile7 } from "node:fs/promises";
131065
+ import { access as access3, readFile as readFile8 } from "node:fs/promises";
130456
131066
  import { homedir as homedir10 } from "node:os";
130457
- import { join as join42 } from "node:path";
131067
+ import { join as join43 } from "node:path";
130458
131068
  var KNOWN_LSP_PLUGINS = ["typescript-lsp@claude-plugins-official"];
130459
131069
  function claudeConfigDir(env = process.env, home = homedir10()) {
130460
- return env.CLAUDE_CONFIG_DIR || join42(home, ".claude");
131070
+ return env.CLAUDE_CONFIG_DIR || join43(home, ".claude");
130461
131071
  }
130462
131072
  async function readJson2(path2) {
130463
131073
  try {
130464
- return JSON.parse(await readFile7(path2, "utf8"));
131074
+ return JSON.parse(await readFile8(path2, "utf8"));
130465
131075
  } catch {
130466
131076
  return void 0;
130467
131077
  }
@@ -130472,22 +131082,22 @@ async function declaresLspServers(id3, configDir) {
130472
131082
  if (at2 <= 0) return false;
130473
131083
  const name = id3.slice(0, at2);
130474
131084
  const marketplace = id3.slice(at2 + 1);
130475
- const installed = await readJson2(join42(configDir, "plugins", "installed_plugins.json"));
131085
+ const installed = await readJson2(join43(configDir, "plugins", "installed_plugins.json"));
130476
131086
  const entries = isObj(installed) && isObj(installed.plugins) ? installed.plugins[id3] : void 0;
130477
131087
  for (const entry of Array.isArray(entries) ? entries : []) {
130478
131088
  const path2 = isObj(entry) && typeof entry.installPath === "string" ? entry.installPath : null;
130479
131089
  if (!path2) continue;
130480
- if (declares(await readJson2(join42(path2, ".claude-plugin", "plugin.json")))) return true;
131090
+ if (declares(await readJson2(join43(path2, ".claude-plugin", "plugin.json")))) return true;
130481
131091
  try {
130482
- await access3(join42(path2, ".lsp.json"));
131092
+ await access3(join43(path2, ".lsp.json"));
130483
131093
  return true;
130484
131094
  } catch {
130485
131095
  }
130486
131096
  }
130487
- const known = await readJson2(join42(configDir, "plugins", "known_marketplaces.json"));
131097
+ const known = await readJson2(join43(configDir, "plugins", "known_marketplaces.json"));
130488
131098
  const place = isObj(known) && isObj(known[marketplace]) ? known[marketplace].installLocation : void 0;
130489
131099
  if (typeof place !== "string") return false;
130490
- const listing = await readJson2(join42(place, ".claude-plugin", "marketplace.json"));
131100
+ const listing = await readJson2(join43(place, ".claude-plugin", "marketplace.json"));
130491
131101
  const plugins = isObj(listing) && Array.isArray(listing.plugins) ? listing.plugins : [];
130492
131102
  return plugins.some((p) => isObj(p) && p.name === name && declares(p));
130493
131103
  }
@@ -130850,8 +131460,8 @@ var MidTurnLedger = class {
130850
131460
  import { resolve as resolve17 } from "node:path";
130851
131461
 
130852
131462
  // packages/agent-host/src/adapters/claude/new-file.ts
130853
- import { stat as stat9 } from "node:fs/promises";
130854
- async function newFile(path2, statOf = stat9) {
131463
+ import { stat as stat10 } from "node:fs/promises";
131464
+ async function newFile(path2, statOf = stat10) {
130855
131465
  const found = statOf(path2).then(
130856
131466
  () => false,
130857
131467
  (err) => err.code === "ENOENT" ? true : void 0
@@ -131378,7 +131988,7 @@ function tracksInputs(m) {
131378
131988
 
131379
131989
  // packages/agent-host/src/adapters/claude/surface.ts
131380
131990
  import { readFileSync as readFileSync16 } from "node:fs";
131381
- import { join as join43 } from "node:path";
131991
+ import { join as join44 } from "node:path";
131382
131992
 
131383
131993
  // packages/agent-host/src/adapters/surface-drift.ts
131384
131994
  function compareSurface(surface, limitations, baseline) {
@@ -131871,8 +132481,8 @@ var CLAUDE_SURFACE = {
131871
132481
  async gather(env) {
131872
132482
  const sdk = await env.packageDir(SDK);
131873
132483
  return {
131874
- "package.json": readFileSync16(join43(sdk, "package.json"), "utf8"),
131875
- "sdk.d.ts": readFileSync16(join43(sdk, "sdk.d.ts"), "utf8"),
132484
+ "package.json": readFileSync16(join44(sdk, "package.json"), "utf8"),
132485
+ "sdk.d.ts": readFileSync16(join44(sdk, "sdk.d.ts"), "utf8"),
131876
132486
  "claude --version": await env.run("claude", ["--version"]),
131877
132487
  "claude --help": await env.run("claude", ["--help"])
131878
132488
  };
@@ -132075,7 +132685,7 @@ var ClaudeSession = class {
132075
132685
  const preset = this.opts.permissionPreset;
132076
132686
  this.cards.editAskRule = preset === "edits" || preset === "normal" ? await editAskRuleExists(this.opts) : true;
132077
132687
  const exe = this.launch.executable();
132078
- const run4 = exe ? this.launch.links.prepare(exe) : null;
132688
+ const run4 = exe ? await this.launch.links.ensure(exe) : null;
132079
132689
  this.servers.load();
132080
132690
  const servers = this.servers.mcpServers();
132081
132691
  const permissionOptions = await permissionOptionsFor(this.opts);
@@ -132092,7 +132702,7 @@ var ClaudeSession = class {
132092
132702
  servers,
132093
132703
  permissionOptions,
132094
132704
  codeIntelligence,
132095
- spawn: (o) => this.opts.processSource ? this.spawnProcess(o) : this.proc = spawnLocalAgent(o),
132705
+ spawn: (o) => this.opts.processSource ? this.spawnProcess(o) : this.proc = tapSessionProcess(spawnLocalAgent(o), this.sessionId),
132096
132706
  canUseTool: (toolName, toolInput, options) => this.cards.canUseTool(preset, toolName, toolInput, options),
132097
132707
  hooks: requestTurnHooks(this.opts, () => this.servers.orchestratorServer !== null)
132098
132708
  })
@@ -132271,7 +132881,7 @@ var ClaudeSession = class {
132271
132881
  const src = this.opts.processSource;
132272
132882
  const adopt = src.adopt && !this.adopted ? src.adopt.process : null;
132273
132883
  this.adopted = true;
132274
- this.proc = adopt ?? src.spawn({ command: o.command, args: o.args, cwd: o.cwd, env: o.env });
132884
+ this.proc = tapSessionProcess(adopt ?? src.spawn({ command: o.command, args: o.args, cwd: o.cwd, env: o.env }), this.sessionId);
132275
132885
  return this.proc;
132276
132886
  }
132277
132887
  /**
@@ -132569,7 +133179,7 @@ var ClaudeSession = class {
132569
133179
  };
132570
133180
 
132571
133181
  // packages/agent-host/src/adapters/claude/index.ts
132572
- var exec4 = promisify4(execFile5);
133182
+ var exec4 = promisify4(execFile6);
132573
133183
  var PROBE_USAGE_MS = 2e4;
132574
133184
  var PROBE_USAGE_RETRY_MS = 500;
132575
133185
  var ACCOUNT_CACHE_MS = 6e4;
@@ -132597,10 +133207,10 @@ async function claudeLoggedIn(bin) {
132597
133207
  async function sweepProbeDirs(now = Date.now()) {
132598
133208
  try {
132599
133209
  const root3 = tmpdir4();
132600
- for (const name of await readdir11(root3)) {
133210
+ for (const name of await readdir12(root3)) {
132601
133211
  if (!name.startsWith(PROBE_DIR_PREFIX)) continue;
132602
- const path2 = join44(root3, name);
132603
- const old = await stat10(path2).then((s) => now - s.mtimeMs > PROBE_DIR_STALE_MS, () => false);
133212
+ const path2 = join45(root3, name);
133213
+ const old = await stat11(path2).then((s) => now - s.mtimeMs > PROBE_DIR_STALE_MS, () => false);
132604
133214
  if (old) await rm9(path2, { recursive: true, force: true }).catch(() => {
132605
133215
  });
132606
133216
  }
@@ -132708,7 +133318,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
132708
133318
  try {
132709
133319
  let launch2;
132710
133320
  try {
132711
- launch2 = this.statusLaunch();
133321
+ launch2 = await this.statusLaunch();
132712
133322
  } catch (err) {
132713
133323
  if (!(err instanceof ClaudePlaceholderError)) throw err;
132714
133324
  return { tool: "claude", installed: false, loggedIn: false, detail: err.message };
@@ -132791,7 +133401,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
132791
133401
  this.swept = true;
132792
133402
  await sweepProbeDirs();
132793
133403
  }
132794
- const dir = await mkdtemp4(join44(tmpdir4(), PROBE_DIR_PREFIX));
133404
+ const dir = await mkdtemp4(join45(tmpdir4(), PROBE_DIR_PREFIX));
132795
133405
  const sessionId = `usage-probe-${randomUUID23()}`;
132796
133406
  _ClaudeAdapter.probeIds.add(sessionId);
132797
133407
  let probe = null;
@@ -132829,7 +133439,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
132829
133439
  if (this.accountCache && Date.now() - this.accountCache.at < ACCOUNT_CACHE_MS) return this.accountCache.value;
132830
133440
  let value;
132831
133441
  try {
132832
- const status = await claudeAuthStatus(this.statusLaunch());
133442
+ const status = await claudeAuthStatus(await this.statusLaunch());
132833
133443
  const flag = status?.loggedIn;
132834
133444
  value = { loggedIn: typeof flag === "boolean" ? flag : null, fingerprint: accountFingerprint(status) };
132835
133445
  } catch {
@@ -132840,9 +133450,9 @@ var ClaudeAdapter = class _ClaudeAdapter {
132840
133450
  }
132841
133451
  accountCache = null;
132842
133452
  /** The program the sign-in is asked of: what sessions start (on Windows the link), else the one on PATH */
132843
- statusLaunch() {
133453
+ async statusLaunch() {
132844
133454
  const exe = this.launch.windows ? this.launch.executable() : null;
132845
- if (exe && /\.exe$/i.test(exe)) return { command: this.launch.links.prepare(exe).path, args: [] };
133455
+ if (exe && /\.exe$/i.test(exe)) return { command: (await this.launch.links.ensure(exe)).path, args: [] };
132846
133456
  return launchFor2(toolCommand("claude"));
132847
133457
  }
132848
133458
  async listModels() {
@@ -132875,13 +133485,13 @@ var ClaudeAdapter = class _ClaudeAdapter {
132875
133485
  };
132876
133486
 
132877
133487
  // packages/agent-host/src/adapters/codex/index.ts
132878
- import { execFile as execFile6 } from "node:child_process";
133488
+ import { execFile as execFile7 } from "node:child_process";
132879
133489
  import { existsSync as existsSync21 } from "node:fs";
132880
- import { join as join48 } from "node:path";
133490
+ import { join as join49 } from "node:path";
132881
133491
  import { promisify as promisify5 } from "node:util";
132882
133492
 
132883
133493
  // packages/agent-host/src/adapters/codex/client.ts
132884
- import { randomBytes as randomBytes10 } from "node:crypto";
133494
+ import { randomBytes as randomBytes11 } from "node:crypto";
132885
133495
 
132886
133496
  // packages/agent-host/src/adapters/codex/native.ts
132887
133497
  import { existsSync as existsSync20, readFileSync as readFileSync17, realpathSync as realpathSync8 } from "node:fs";
@@ -133010,7 +133620,7 @@ var CodexClient = class {
133010
133620
  * with plain counters both hosts would have sent `1`, `2`, … and an old answer would resolve a new
133011
133621
  * request. codex echoes string ids back as given.
133012
133622
  */
133013
- idPrefix = `${randomBytes10(3).toString("hex")}-`;
133623
+ idPrefix = `${randomBytes11(3).toString("hex")}-`;
133014
133624
  pending = new PendingRequests();
133015
133625
  closed = false;
133016
133626
  /** A flag to call onExit only once (both 'error' and 'exit' can fire) */
@@ -133107,24 +133717,24 @@ async function withCodexClient(opts, fn2) {
133107
133717
 
133108
133718
  // packages/agent-host/src/adapters/codex/home.ts
133109
133719
  import { homedir as homedir11 } from "node:os";
133110
- import { join as join45 } from "node:path";
133720
+ import { join as join46 } from "node:path";
133111
133721
  function codexHome(env = process.env, home = homedir11()) {
133112
133722
  const custom2 = env.CODEX_HOME?.trim();
133113
- return custom2 ? custom2 : join45(home, ".codex");
133723
+ return custom2 ? custom2 : join46(home, ".codex");
133114
133724
  }
133115
133725
 
133116
133726
  // packages/agent-host/src/adapters/codex/rollout.ts
133117
133727
  import { createReadStream as createReadStream5 } from "node:fs";
133118
- import { readdir as readdir12 } from "node:fs/promises";
133119
- import { join as join46 } from "node:path";
133728
+ import { readdir as readdir13 } from "node:fs/promises";
133729
+ import { join as join47 } from "node:path";
133120
133730
  var MIN_SUMMARY_CHARS = 200;
133121
- async function findRolloutPath(threadId, sessionsDir = join46(codexHome(), "sessions")) {
133731
+ async function findRolloutPath(threadId, sessionsDir = join47(codexHome(), "sessions")) {
133122
133732
  try {
133123
133733
  const suffix = `-${threadId}.jsonl`;
133124
- const names = await readdir12(sessionsDir, { recursive: true });
133734
+ const names = await readdir13(sessionsDir, { recursive: true });
133125
133735
  const hits = names.filter((n) => String(n).endsWith(suffix)).sort();
133126
133736
  const hit = hits[hits.length - 1];
133127
- return hit ? join46(sessionsDir, String(hit)) : null;
133737
+ return hit ? join47(sessionsDir, String(hit)) : null;
133128
133738
  } catch {
133129
133739
  return null;
133130
133740
  }
@@ -133602,7 +134212,7 @@ function normalizeNotification(sessionId, n) {
133602
134212
  * exactly the lie we are trying to fix. The same rule the Claude adapter uses for `result`.
133603
134213
  *
133604
134214
  * An interrupted turn is not a finished one either (#698). It went out as `turn_complete`, so a turn Codex cut
133605
- * short told the orchestrator that gave it "The assigned work finished". It ends as the person's Stop ends a turn,
134215
+ * short told the orchestrator that gave it that the work was done ("finished", as the report then read). It ends as the person's Stop ends a turn,
133606
134216
  * `waiting_input` with `interrupted`; the adapter tells a Stop of ours from an interrupt it did not ask for
133607
134217
  * (`INTERRUPTED_BY_TOOL`).
133608
134218
  */
@@ -135627,7 +136237,7 @@ var CodexSession = class {
135627
136237
 
135628
136238
  // packages/agent-host/src/adapters/codex/surface.ts
135629
136239
  import { readFileSync as readFileSync18 } from "node:fs";
135630
- import { join as join47 } from "node:path";
136240
+ import { join as join48 } from "node:path";
135631
136241
 
135632
136242
  // packages/agent-host/src/adapters/codex/surface-baseline.ts
135633
136243
  var SURFACE_BASELINE2 = {
@@ -135945,7 +136555,7 @@ var CODEX_SURFACE = {
135945
136555
  "codex --version": await env.run("codex", ["--version"]),
135946
136556
  "codex --help": await env.run("codex", ["--help"])
135947
136557
  };
135948
- for (const f of FILES) inputs[f] = readFileSync18(join47(out, f), "utf8");
136558
+ for (const f of FILES) inputs[f] = readFileSync18(join48(out, f), "utf8");
135949
136559
  return inputs;
135950
136560
  },
135951
136561
  read: readCodexSurface,
@@ -135954,7 +136564,7 @@ var CODEX_SURFACE = {
135954
136564
  };
135955
136565
 
135956
136566
  // packages/agent-host/src/adapters/codex/index.ts
135957
- var exec5 = promisify5(execFile6);
136567
+ var exec5 = promisify5(execFile7);
135958
136568
  var CODEX_VERBOSITIES = ["low", "medium", "high"];
135959
136569
  var CodexAdapter = class {
135960
136570
  tool = "codex";
@@ -136013,7 +136623,7 @@ var CodexAdapter = class {
136013
136623
  const launch2 = launchFor2(path2 ?? toolCommand("codex"));
136014
136624
  const { stdout } = await exec5(launch2.command, [...launch2.args, "--version"], { timeout: 5e3, windowsHide: true });
136015
136625
  const version2 = `${stdout.trim()} \xB7 ${path2 ?? "PATH"}`;
136016
- const loggedIn = existsSync21(join48(codexHome(), "auth.json"));
136626
+ const loggedIn = existsSync21(join49(codexHome(), "auth.json"));
136017
136627
  return {
136018
136628
  tool: "codex",
136019
136629
  installed: true,
@@ -136116,12 +136726,12 @@ var CodexAdapter = class {
136116
136726
  };
136117
136727
 
136118
136728
  // packages/agent-host/src/adapters/cursor-agent/index.ts
136119
- import { execFile as execFile8 } from "node:child_process";
136729
+ import { execFile as execFile9 } from "node:child_process";
136120
136730
  import { promisify as promisify7 } from "node:util";
136121
136731
 
136122
136732
  // packages/agent-host/src/adapters/cursor-agent/install.ts
136123
136733
  import { existsSync as existsSync22, readdirSync as readdirSync14, realpathSync as realpathSync10 } from "node:fs";
136124
- import { dirname as dirname22, join as join49, win32 as win327 } from "node:path";
136734
+ import { dirname as dirname22, join as join50, win32 as win327 } from "node:path";
136125
136735
  var realFs = {
136126
136736
  exists: existsSync22,
136127
136737
  list: (dir) => {
@@ -136173,15 +136783,15 @@ function findCursor(env = process.env, platform, fs3 = realFs) {
136173
136783
  const real3 = fs3.real(found);
136174
136784
  const dir = dirname22(real3);
136175
136785
  const version2 = dir.split(/[\\/]/).pop() ?? "";
136176
- if (VERSION.test(version2) && fs3.exists(join49(dir, "index.js"))) return { version: version2, command: real3, args: [], env: {} };
136786
+ if (VERSION.test(version2) && fs3.exists(join50(dir, "index.js"))) return { version: version2, command: real3, args: [], env: {} };
136177
136787
  }
136178
136788
  return null;
136179
136789
  }
136180
136790
 
136181
136791
  // packages/agent-host/src/adapters/cursor-agent/surface.ts
136182
- import { execFile as execFile7 } from "node:child_process";
136792
+ import { execFile as execFile8 } from "node:child_process";
136183
136793
  import { readdirSync as readdirSync15, readFileSync as readFileSync19 } from "node:fs";
136184
- import { dirname as dirname23, join as join50 } from "node:path";
136794
+ import { dirname as dirname23, join as join51 } from "node:path";
136185
136795
  import { promisify as promisify6 } from "node:util";
136186
136796
 
136187
136797
  // packages/agent-host/src/adapters/cursor-agent/surface-baseline.ts
@@ -136241,7 +136851,7 @@ var SURFACE_BASELINE3 = {
136241
136851
  };
136242
136852
 
136243
136853
  // packages/agent-host/src/adapters/cursor-agent/surface.ts
136244
- var exec6 = promisify6(execFile7);
136854
+ var exec6 = promisify6(execFile8);
136245
136855
  async function gather2() {
136246
136856
  const install = findCursor();
136247
136857
  if (!install) throw new Error("Cursor CLI is not installed on this machine (https://cursor.com/install)");
@@ -136251,7 +136861,7 @@ async function gather2() {
136251
136861
  const dir = dirname23(install.args[0] ?? install.command);
136252
136862
  const names = /* @__PURE__ */ new Set();
136253
136863
  for (const f of readdirSync15(dir).filter((n) => n.endsWith(".js"))) {
136254
- const text3 = readFileSync19(join50(dir, f), "utf8");
136864
+ const text3 = readFileSync19(join51(dir, f), "utf8");
136255
136865
  if (!text3.includes("session/request_permission") && !text3.includes("cursor_login")) continue;
136256
136866
  for (const m of text3.matchAll(/sessionUpdate:"([a-z_]+)"|"(cursor\/[a-z_]+)"/g)) {
136257
136867
  const name = m[1] ?? m[2];
@@ -136435,7 +137045,7 @@ function cursorUsageReader(run4, now = Date.now) {
136435
137045
  }
136436
137046
 
136437
137047
  // packages/agent-host/src/adapters/cursor-agent/index.ts
136438
- var exec7 = promisify7(execFile8);
137048
+ var exec7 = promisify7(execFile9);
136439
137049
  var CAPABILITIES2 = {
136440
137050
  ...ACP_CAPABILITIES,
136441
137051
  contextUsage: "none",
@@ -136499,24 +137109,24 @@ var CursorAgentAdapter = class extends AcpAdapter {
136499
137109
  };
136500
137110
 
136501
137111
  // packages/agent-host/src/adapters/opencode/index.ts
136502
- import { execFile as execFile10 } from "node:child_process";
137112
+ import { execFile as execFile11 } from "node:child_process";
136503
137113
  import { promisify as promisify9 } from "node:util";
136504
137114
 
136505
137115
  // packages/agent-host/src/adapters/opencode/config-files.ts
136506
137116
  import { existsSync as existsSync23, readdirSync as readdirSync16, readFileSync as readFileSync20 } from "node:fs";
136507
137117
  import { homedir as homedir15 } from "node:os";
136508
- import { dirname as dirname24, extname as extname3, join as join51, relative as relative6 } from "node:path";
137118
+ import { dirname as dirname24, extname as extname3, join as join52, relative as relative6 } from "node:path";
136509
137119
  function readOpenCodeConfigs(cwd, env, home = homedir15()) {
136510
- const global2 = join51(env.XDG_CONFIG_HOME || join51(home, ".config"), "opencode");
137120
+ const global2 = join52(env.XDG_CONFIG_HOME || join52(home, ".config"), "opencode");
136511
137121
  const project2 = !env.OPENCODE_DISABLE_PROJECT_CONFIG;
136512
137122
  const upward = project2 ? foldersUp(cwd) : [];
136513
- const dotFolders = [...upward.map((d2) => join51(d2, ".opencode")), join51(home, ".opencode"), ...env.OPENCODE_CONFIG_DIR ? [env.OPENCODE_CONFIG_DIR] : []];
137123
+ const dotFolders = [...upward.map((d2) => join52(d2, ".opencode")), join52(home, ".opencode"), ...env.OPENCODE_CONFIG_DIR ? [env.OPENCODE_CONFIG_DIR] : []];
136514
137124
  const files2 = [
136515
- ...["config.json", "opencode.json", "opencode.jsonc"].map((f) => join51(global2, f)),
137125
+ ...["config.json", "opencode.json", "opencode.jsonc"].map((f) => join52(global2, f)),
136516
137126
  ...env.OPENCODE_CONFIG ? [env.OPENCODE_CONFIG] : [],
136517
- ...upward.flatMap((d2) => CONFIG_NAMES.map((f) => join51(d2, f))),
136518
- ...dotFolders.flatMap((d2) => CONFIG_NAMES.map((f) => join51(d2, f))),
136519
- ...CONFIG_NAMES.map((f) => join51(managedFolder(env), f))
137127
+ ...upward.flatMap((d2) => CONFIG_NAMES.map((f) => join52(d2, f))),
137128
+ ...dotFolders.flatMap((d2) => CONFIG_NAMES.map((f) => join52(d2, f))),
137129
+ ...CONFIG_NAMES.map((f) => join52(managedFolder(env), f))
136520
137130
  ];
136521
137131
  return {
136522
137132
  layers: files2.map(readConfig).filter((c) => c !== null),
@@ -136528,7 +137138,7 @@ function foldersUp(cwd) {
136528
137138
  const out = [];
136529
137139
  for (let d2 = cwd; ; d2 = dirname24(d2)) {
136530
137140
  out.push(d2);
136531
- if (existsSync23(join51(d2, ".git")) || dirname24(d2) === d2) return out;
137141
+ if (existsSync23(join52(d2, ".git")) || dirname24(d2) === d2) return out;
136532
137142
  }
136533
137143
  }
136534
137144
  function managedFolder(env) {
@@ -136570,13 +137180,13 @@ function stringEnd(text3, start3) {
136570
137180
  return text3.length;
136571
137181
  }
136572
137182
  function agentNames(folder) {
136573
- const named = (sub, deep) => markdown(join51(folder, sub), deep).map((f) => relative6(join51(folder, sub), f).replaceAll("\\", "/").slice(0, -extname3(f).length));
137183
+ const named = (sub, deep) => markdown(join52(folder, sub), deep).map((f) => relative6(join52(folder, sub), f).replaceAll("\\", "/").slice(0, -extname3(f).length));
136574
137184
  return [...named("agent", true), ...named("agents", true), ...named("mode", false), ...named("modes", false)];
136575
137185
  }
136576
137186
  function markdown(folder, deep) {
136577
137187
  try {
136578
137188
  return readdirSync16(folder, { withFileTypes: true }).flatMap((e) => {
136579
- const path2 = join51(folder, e.name);
137189
+ const path2 = join52(folder, e.name);
136580
137190
  if (e.isDirectory()) return deep ? markdown(path2, deep) : [];
136581
137191
  return e.name.endsWith(".md") ? [path2] : [];
136582
137192
  });
@@ -136705,7 +137315,7 @@ function openCodeRefusal(line, conversation) {
136705
137315
  }
136706
137316
 
136707
137317
  // packages/agent-host/src/adapters/opencode/surface.ts
136708
- import { execFile as execFile9 } from "node:child_process";
137318
+ import { execFile as execFile10 } from "node:child_process";
136709
137319
  import { readFileSync as readFileSync21 } from "node:fs";
136710
137320
  import { promisify as promisify8 } from "node:util";
136711
137321
 
@@ -136762,7 +137372,7 @@ var SURFACE_BASELINE4 = {
136762
137372
  };
136763
137373
 
136764
137374
  // packages/agent-host/src/adapters/opencode/surface.ts
136765
- var exec8 = promisify8(execFile9);
137375
+ var exec8 = promisify8(execFile10);
136766
137376
  async function gather3() {
136767
137377
  const install = findOpenCode();
136768
137378
  if (!install) throw new Error("OpenCode is not installed on this machine (npm i -g opencode-ai)");
@@ -136905,7 +137515,7 @@ function openCodeUsageReader(run4, now = Date.now) {
136905
137515
  }
136906
137516
 
136907
137517
  // packages/agent-host/src/adapters/opencode/index.ts
136908
- var exec9 = promisify9(execFile10);
137518
+ var exec9 = promisify9(execFile11);
136909
137519
  var CAPABILITIES3 = {
136910
137520
  ...ACP_CAPABILITIES
136911
137521
  };
@@ -137078,7 +137688,7 @@ var ApprovalRules = class {
137078
137688
  };
137079
137689
 
137080
137690
  // packages/agent-host/src/sessions/project-registry.ts
137081
- import { basename as basename12 } from "node:path";
137691
+ import { basename as basename13 } from "node:path";
137082
137692
  var ProjectRegistry = class {
137083
137693
  constructor(host) {
137084
137694
  this.host = host;
@@ -137132,7 +137742,7 @@ var ProjectRegistry = class {
137132
137742
  return {
137133
137743
  id: id3,
137134
137744
  path: path2,
137135
- name: basename12(path2),
137745
+ name: basename13(path2),
137136
137746
  defaultTool: this.host.firstTool(stored),
137137
137747
  // Trust (M4, decision 3) — reads the same column as the app runtime. The trust toggle on screen shows
137138
137748
  // this value
@@ -137401,11 +138011,11 @@ var SessionLayout = class {
137401
138011
  // packages/agent-host/src/sessions/linked-reports.ts
137402
138012
  var ADD_NEXT_MS = 15e3;
137403
138013
  function partOf(turn) {
137404
- return turn.wrote || turn.added ? { ...turn.wrote ? { wrote: true } : {}, ...turn.added ? { added: true } : {} } : void 0;
138014
+ return turn.wrote || turn.added || turn.cut ? { ...turn.wrote ? { wrote: true } : {}, ...turn.added ? { added: true } : {}, ...turn.cut ? { cut: true } : {} } : void 0;
137405
138015
  }
137406
138016
  function saidTurn(raw) {
137407
138017
  if (typeof raw !== "object" || raw === null || !("by" in raw) || raw.by !== "person" && raw.by !== "work") return void 0;
137408
- return { by: raw.by, ..."wrote" in raw && raw.wrote === true ? { wrote: true } : {}, ..."added" in raw && raw.added === true ? { added: true } : {} };
138018
+ return { by: raw.by, ..."wrote" in raw && raw.wrote === true ? { wrote: true } : {}, ..."added" in raw && raw.added === true ? { added: true } : {}, ..."cut" in raw && raw.cut === true ? { cut: true } : {} };
137409
138019
  }
137410
138020
  function fromHub(from) {
137411
138021
  const id3 = from?.sessionId;
@@ -137493,7 +138103,7 @@ var LinkedReports = class {
137493
138103
  wait.paused = [...held3, ...running2.filter((t) => !held3.some((h) => h.id === t.id))];
137494
138104
  wait.busy = false;
137495
138105
  this.host.children.setLinkedReport(sessionId, wait);
137496
- if (!told) void this.report(sessionId, wait.orchestratorId, e, { paused: running2, stoppable: false }, void 0, partOf(turn));
138106
+ if (!told) void this.report(sessionId, wait.orchestratorId, e, { paused: running2, stoppable: false }, void 0, partOf(turn), false, wait);
137497
138107
  return;
137498
138108
  }
137499
138109
  this.drop(sessionId);
@@ -137688,7 +138298,7 @@ var LinkedReports = class {
137688
138298
  continue;
137689
138299
  }
137690
138300
  this.drop(sessionId);
137691
- void this.report(sessionId, wait.orchestratorId, { type: "turn_complete" }, { linkLost: name, paused: wait.paused });
138301
+ void this.report(sessionId, wait.orchestratorId, { type: "turn_complete" }, { linkLost: name, paused: wait.paused }, void 0, void 0, false, wait);
137692
138302
  }
137693
138303
  for (const sessionId of [...this.tasks.keys()]) if (sessionId.startsWith(prefix)) this.forget(sessionId);
137694
138304
  }
@@ -137724,23 +138334,25 @@ var LinkedReports = class {
137724
138334
  if (!sessionId.startsWith(prefix)) continue;
137725
138335
  const about = this.about.get(sessionId);
137726
138336
  this.drop(sessionId);
137727
- void this.report(sessionId, wait.orchestratorId, { type: "turn_complete" }, { machineRemoved: name, ...wait.paused ? { paused: wait.paused } : {} }, about);
138337
+ void this.report(sessionId, wait.orchestratorId, { type: "turn_complete" }, { machineRemoved: name, ...wait.paused ? { paused: wait.paused } : {} }, about, void 0, false, wait);
137728
138338
  }
137729
138339
  for (const sessionId of [...this.tasks.keys()]) if (sessionId.startsWith(prefix)) this.forget(sessionId);
137730
138340
  this.machineNames.delete(machineId);
137731
138341
  }
137732
- async report(sessionId, orchestratorId, ending, work, known, part, theirs = false) {
138342
+ async report(sessionId, orchestratorId, ending, work, known, part, theirs = false, wait) {
137733
138343
  const reader = this.host.linkedReader;
137734
138344
  if (!reader || !this.host.hasSession(orchestratorId)) return;
137735
138345
  const about = reader.describe(sessionId) ?? known ?? this.about.get(sessionId) ?? { name: sessionId, project: "(unknown)" };
137736
138346
  const unreachable = work && ("linkLost" in work || "machineRemoved" in work);
138347
+ const kept = unreachable && work && "paused" in work && work.paused ? wait?.read : void 0;
137737
138348
  if (!unreachable && this.gate(sessionId) !== "deliver") return;
137738
138349
  const tookOver = !!work && "tookOver" in work || theirs;
137739
138350
  const since = turnSince(ending);
137740
138351
  const words = unreachable || tookOver ? null : await reader.lastWords(sessionId, 600, { turn: true, ...since !== void 0 ? { since } : {} }).catch((e) => ({ ok: false, error: e.message }));
137741
- const preview = theirs && !(work && "tookOver" in work) ? "(not read: the last turn there was the person's; read_session shows it)" : tookOver ? "(not read: the conversation there is the person's now; read_session shows it)" : !words ? work && "paused" in work && work.paused ? "(in the paused report before this one)" : "(not read: its machine could not be reached)" : words.ok ? words.text === "" ? NO_WORDS : words.text : `(its last response could not be read there: ${words.error})`;
137742
- const seq = words?.ok && words.seq !== null ? words.seq : void 0;
137743
- await this.host.deliverReport(orchestratorId, sessionId, about.name, about.project, preview, ending, work, seq, part);
138352
+ const preview = theirs && !(work && "tookOver" in work) ? "(not read: the last turn there was the person's; read_session shows it)" : tookOver ? "(not read: the conversation there is the person's now; read_session shows it)" : !words ? kept ? clipPreview(kept.preview, 600) : work && "paused" in work && work.paused ? IN_PAUSE_REPORT : MACHINE_UNREACHABLE : words.ok ? words.text === "" ? NO_WORDS : words.text : `(its last response could not be read there: ${words.error})`;
138353
+ if (wait && words?.ok && words.text !== "" && work && "paused" in work && "stoppable" in work) wait.read = { preview: words.text, seq: words.seq ?? void 0 };
138354
+ const seq = words?.ok ? words.seq ?? void 0 : kept?.seq;
138355
+ await this.host.deliverReport(orchestratorId, sessionId, about.name, about.project, preview, ending, kept && unreachable ? { ...work, asRead: true } : work, seq, part);
137744
138356
  }
137745
138357
  };
137746
138358
 
@@ -138627,7 +139239,7 @@ var UsageRecorder = class {
138627
139239
  }
138628
139240
  /** Counts one request in its hour's bucket, in memory until the next flush */
138629
139241
  request(src, r, counted, at2 = this.now()) {
138630
- const hour = Math.floor(at2 / HOUR_MS) * HOUR_MS;
139242
+ const hour = Math.floor(at2 / HOUR_MS2) * HOUR_MS2;
138631
139243
  const projectId = src.projectId ?? "";
138632
139244
  const model = r.model || src.model || "";
138633
139245
  const id3 = [hour, src.sessionId, projectId, src.tool, model].join("\n");
@@ -138697,10 +139309,10 @@ var UsageRecorder = class {
138697
139309
  /** Drops what is older than the keeping time. Once a day at most unless `force` */
138698
139310
  prune(force = false) {
138699
139311
  const now = this.now();
138700
- if (!force && now - this.lastPrune < 24 * HOUR_MS) return;
139312
+ if (!force && now - this.lastPrune < 24 * HOUR_MS2) return;
138701
139313
  this.lastPrune = now;
138702
139314
  try {
138703
- this.usage.prune(now - KEEP_DAYS * 24 * HOUR_MS);
139315
+ this.usage.prune(now - KEEP_DAYS * 24 * HOUR_MS2);
138704
139316
  } catch (err) {
138705
139317
  console.error(`[usage] could not prune: ${errorMessage(err)}`);
138706
139318
  }
@@ -138747,7 +139359,7 @@ var UsageRecorder = class {
138747
139359
 
138748
139360
  // packages/agent-host/src/sessions/manager.ts
138749
139361
  import { homedir as homedir17 } from "node:os";
138750
- import { join as join54 } from "node:path";
139362
+ import { join as join55 } from "node:path";
138751
139363
 
138752
139364
  // packages/agent-host/src/dev-services/store.ts
138753
139365
  import Database4 from "better-sqlite3";
@@ -143445,6 +144057,105 @@ var Store = class extends MigrationRunner {
143445
144057
  };
143446
144058
  var WAL_SIZE_LIMIT = 64 * 1024 * 1024;
143447
144059
 
144060
+ // packages/agent-host/src/lifecycle/system-end.ts
144061
+ var SYSTEM_END_FILE = "system-end.json";
144062
+ var SystemEndNotice = class {
144063
+ constructor(o) {
144064
+ this.o = o;
144065
+ }
144066
+ o;
144067
+ at = null;
144068
+ /** Which notice an expiry belongs to: a later one is not called off by an earlier one's timer */
144069
+ notices = 0;
144070
+ closed = false;
144071
+ /** The system said it is ending, at `at` */
144072
+ notice(at2 = (this.o.now ?? Date.now)()) {
144073
+ if (this.at !== null) return;
144074
+ this.at = at2;
144075
+ const n = ++this.notices;
144076
+ this.flush();
144077
+ const setTimer = this.o.setTimer ?? ((f, ms3) => void setTimeout(f, ms3).unref());
144078
+ setTimer(() => {
144079
+ if (this.notices !== n || this.at === null) return;
144080
+ this.o.outlived?.();
144081
+ this.cancel();
144082
+ }, SYSTEM_END_TAIL_MS);
144083
+ }
144084
+ /** The end was called off (Windows: another app refused it) */
144085
+ cancel() {
144086
+ if (this.at === null) return;
144087
+ this.at = null;
144088
+ this.flush();
144089
+ }
144090
+ close() {
144091
+ this.closed = true;
144092
+ }
144093
+ flush() {
144094
+ if (!this.closed) this.o.write(this.at === null ? null : { at: this.at });
144095
+ }
144096
+ };
144097
+ function parseSystemEnd(text3) {
144098
+ let r;
144099
+ try {
144100
+ r = JSON.parse(text3);
144101
+ } catch {
144102
+ return null;
144103
+ }
144104
+ if (typeof r !== "object" || r === null || !("v" in r) || r.v !== 1 || !("at" in r) || typeof r.at !== "number") return null;
144105
+ return { at: r.at };
144106
+ }
144107
+ var systemEndText = (end) => `${JSON.stringify({ v: 1, at: end.at })}
144108
+ `;
144109
+ var SYSTEM_END_LEAD_MS = 3e4;
144110
+ var SYSTEM_END_TAIL_MS = 2 * 6e4;
144111
+ var inSystemEnd = (end, ts) => !!end && ts >= end.at - SYSTEM_END_LEAD_MS && ts <= end.at + SYSTEM_END_TAIL_MS;
144112
+ var cutBySystemEnd = (end, lastTs) => !!end && (lastTs === void 0 || lastTs <= end.at + SYSTEM_END_TAIL_MS);
144113
+ function errorCodeOf(payload) {
144114
+ if (typeof payload !== "object" || payload === null || !("type" in payload) || payload.type !== "error") return void 0;
144115
+ const error51 = "error" in payload ? payload.error : void 0;
144116
+ return typeof error51 === "object" && error51 !== null && "code" in error51 && typeof error51.code === "string" ? error51.code : "";
144117
+ }
144118
+ function crashedInSystemEnd(s, rows3, end) {
144119
+ if (s.state !== "error" || !end) return false;
144120
+ const last = rows3.findLast((r) => {
144121
+ const code2 = errorCodeOf(r.payload);
144122
+ return code2 !== void 0 && code2 !== "internal";
144123
+ });
144124
+ return !!last && errorCodeOf(last.payload) === "adapter_crashed" && inSystemEnd(end, last.ts);
144125
+ }
144126
+ function askSystemStopping(o = {}) {
144127
+ const queries = (o.queries === void 0 ? systemStoppingQueries() : o.queries) ?? [];
144128
+ const children = /* @__PURE__ */ new Set();
144129
+ const end = () => {
144130
+ for (const child of children) child.kill("SIGKILL");
144131
+ children.clear();
144132
+ };
144133
+ if (queries.length === 0) return { stopping: null, end };
144134
+ const timeoutMs = o.timeoutMs ?? 1e3;
144135
+ const run4 = o.run ?? ((command, args, options) => realOs().processes.run(command, args, options));
144136
+ const stopping = new Promise((resolve20) => {
144137
+ let left = queries.length;
144138
+ const answered = (yes) => {
144139
+ if (yes) resolve20(true);
144140
+ else if (--left === 0) resolve20(false);
144141
+ };
144142
+ for (const [command, ...args] of queries) {
144143
+ if (command === void 0) {
144144
+ answered(false);
144145
+ continue;
144146
+ }
144147
+ run4(command, args, { timeoutMs, onChild: (child) => void children.add(child) }).then(
144148
+ (r) => answered(String(r.stdout).trim() === "stopping"),
144149
+ () => answered(false)
144150
+ );
144151
+ }
144152
+ setTimeout(() => resolve20(false), timeoutMs).unref();
144153
+ });
144154
+ stopping.finally(end).catch(() => {
144155
+ });
144156
+ return { stopping, end };
144157
+ }
144158
+
143448
144159
  // packages/agent-host/src/lifecycle/update-ended.ts
143449
144160
  var ENDED_MAX_AGE_MS = 10 * 6e4;
143450
144161
  var MAX_ENTRIES2 = 100;
@@ -143841,8 +144552,8 @@ async function gitBranchPr(projectCwd, branch, signal, timeoutMs = OK2.timeout)
143841
144552
  }
143842
144553
 
143843
144554
  // packages/agent-host/src/dev-services/file-search.ts
143844
- import { readdir as readdir13 } from "node:fs/promises";
143845
- import { join as join52 } from "node:path";
144555
+ import { readdir as readdir14 } from "node:fs/promises";
144556
+ import { join as join53 } from "node:path";
143846
144557
  var MAX_FILES2 = 2e4;
143847
144558
  var TTL_MS2 = 15e3;
143848
144559
  var WALK_DEPTH = 6;
@@ -143872,7 +144583,7 @@ async function walk(root3) {
143872
144583
  const { dir, rel, depth } = next;
143873
144584
  let entries;
143874
144585
  try {
143875
- entries = await readdir13(dir, { withFileTypes: true });
144586
+ entries = await readdir14(dir, { withFileTypes: true });
143876
144587
  } catch {
143877
144588
  continue;
143878
144589
  }
@@ -143880,7 +144591,7 @@ async function walk(root3) {
143880
144591
  if (e.name.startsWith(".") && e.name !== ".env") continue;
143881
144592
  if (e.isDirectory()) {
143882
144593
  if (SKIP.has(e.name) || depth >= WALK_DEPTH) continue;
143883
- queue.push({ dir: join52(dir, e.name), rel: wireJoin(rel, e.name), depth: depth + 1 });
144594
+ queue.push({ dir: join53(dir, e.name), rel: wireJoin(rel, e.name), depth: depth + 1 });
143884
144595
  } else {
143885
144596
  out.push(wireJoin(rel, e.name).normalize("NFC"));
143886
144597
  if (out.length >= MAX_FILES2) break;
@@ -145065,14 +145776,14 @@ async function readMessageImage(abs, max = IMAGE_PREVIEW_MAX_BYTES) {
145065
145776
  }
145066
145777
 
145067
145778
  // packages/agent-host/src/dev-services/attachments.ts
145068
- import { lstat as lstat11, mkdir as mkdir8, readdir as readdir14, rm as rm10, writeFile as writeFile6 } from "node:fs/promises";
145069
- import { extname as extname4, join as join53 } from "node:path";
145070
- var root2 = () => join53(dataRoot(), "attachments");
145779
+ import { lstat as lstat11, mkdir as mkdir9, readdir as readdir15, rm as rm10, writeFile as writeFile6 } from "node:fs/promises";
145780
+ import { extname as extname4, join as join54 } from "node:path";
145781
+ var root2 = () => join54(dataRoot(), "attachments");
145071
145782
  function sessionDir(sessionId) {
145072
145783
  if (!isSessionId(sessionId)) {
145073
145784
  throw rpcError(`Not a session id: ${sessionId}`);
145074
145785
  }
145075
- return join53(root2(), sessionId);
145786
+ return join54(root2(), sessionId);
145076
145787
  }
145077
145788
  var EXT = {
145078
145789
  "image/png": ".png",
@@ -145082,9 +145793,9 @@ var EXT = {
145082
145793
  };
145083
145794
  async function saveAttachment(sessionId, name, mime, dataBase64) {
145084
145795
  const dir = sessionDir(sessionId);
145085
- await mkdir8(dir, { recursive: true });
145796
+ await mkdir9(dir, { recursive: true });
145086
145797
  const ext2 = extname4(name) || EXT[mime] || "";
145087
- const file2 = join53(dir, `${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext2}`);
145798
+ const file2 = join54(dir, `${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext2}`);
145088
145799
  const buf = Buffer.from(dataBase64, "base64");
145089
145800
  await writeFile6(file2, buf);
145090
145801
  return { kind: mime.startsWith("image/") ? "image" : "file", path: file2, name, mime, bytes: buf.length };
@@ -145100,15 +145811,15 @@ async function sweepAttachments(maxBytes = ATTACHMENTS_MAX_BYTES) {
145100
145811
  const files2 = [];
145101
145812
  let dirs;
145102
145813
  try {
145103
- dirs = await readdir14(root2());
145814
+ dirs = await readdir15(root2());
145104
145815
  } catch {
145105
145816
  return 0;
145106
145817
  }
145107
145818
  for (const d2 of dirs) {
145108
- const dir = join53(root2(), d2);
145109
- const names = await readdir14(dir).catch(() => []);
145819
+ const dir = join54(root2(), d2);
145820
+ const names = await readdir15(dir).catch(() => []);
145110
145821
  for (const name of names) {
145111
- const p = join53(dir, name);
145822
+ const p = join54(dir, name);
145112
145823
  const s = await lstat11(p).catch(() => null);
145113
145824
  if (s?.isFile()) files2.push({ path: p, size: s.size, mtime: s.mtimeMs });
145114
145825
  }
@@ -146061,7 +146772,7 @@ async function gitStatusForApp(summary, files2) {
146061
146772
  }
146062
146773
 
146063
146774
  // packages/agent-host/src/sessions/session-trash.ts
146064
- import { basename as basename13 } from "node:path";
146775
+ import { basename as basename14 } from "node:path";
146065
146776
  import { rm as rm11 } from "node:fs/promises";
146066
146777
  import { existsSync as existsSync26, statSync as statSync16 } from "node:fs";
146067
146778
  var SessionTrash = class {
@@ -146438,7 +147149,7 @@ var SessionTrash = class {
146438
147149
  this.host.store.projects.addProject({
146439
147150
  id: record2.projectId,
146440
147151
  path: record2.projectPath,
146441
- name: record2.projectName ?? basename13(record2.projectPath)
147152
+ name: record2.projectName ?? basename14(record2.projectPath)
146442
147153
  });
146443
147154
  projectId = record2.projectId;
146444
147155
  registered = await this.host.projectInfo(projectId, record2.projectPath);
@@ -146811,7 +147522,7 @@ function stands(o, proto) {
146811
147522
  return Object.getPrototypeOf(o) === proto;
146812
147523
  }
146813
147524
  var SessionManager = class {
146814
- constructor(store2, adapters2, emit, endpoint, worktreeRoot = join54(homedir17(), DATA_DIR, "worktrees"), keeperOpts = {}) {
147525
+ constructor(store2, adapters2, emit, endpoint, worktreeRoot = join55(homedir17(), DATA_DIR, "worktrees"), keeperOpts = {}) {
146815
147526
  this.store = store2;
146816
147527
  this.adapters = adapters2;
146817
147528
  this.emit = emit;
@@ -146943,16 +147654,21 @@ var SessionManager = class {
146943
147654
  const cut3 = turnCut(s) && !keeperOpts.keptSessions?.has(s.id);
146944
147655
  const goesOn = cut3 && !!lists?.cut(s.id, s.lastSeq);
146945
147656
  const stale = cut3 && !goesOn;
146946
- const fixed = stale ? { ...s, state: "idle", waitingSince: null } : goesOn ? { ...s, state: "working", waitingSince: null } : s;
147657
+ const end = keeperOpts.systemEnd;
147658
+ const recent = end && (stale || s.state === "error") ? store2.messages.loadMessages(s.id, 20) : [];
147659
+ const crashedAtEnd = !cut3 && crashedInSystemEnd(s, recent, end);
147660
+ const lost = stale || crashedAtEnd;
147661
+ const bySystem = crashedAtEnd || stale && cutBySystemEnd(end, recent.at(-1)?.ts);
147662
+ const fixed = lost ? { ...s, state: "idle", waitingSince: null } : goesOn ? { ...s, state: "working", waitingSince: null } : s;
146947
147663
  this.meta.set(s.id, withHubMark(store2.settings, fixed));
146948
- if (stale) {
146949
- console.error(`[agent-host] stale state reset: ${s.id.slice(0, 8)} ${s.state} -> idle`);
146950
- this.interruptions.markStoppedByQuit(fixed, false);
147664
+ if (lost) {
147665
+ console.error(`[agent-host] stale state reset: ${s.id.slice(0, 8)} ${s.state} -> idle${bySystem ? " (the system was ending)" : ""}`);
147666
+ this.interruptions.markTurnLost(fixed, false, bySystem ? "shutdown" : "quit");
146951
147667
  store2.sessions.upsertSession(fixed);
146952
147668
  cutTurns2.set(s.id, this.children.processGone(s.id));
146953
147669
  stoppedByQuit.push(fixed);
146954
147670
  }
146955
- const stopped = stale ? null : lists?.stoppedOf(s.id, s.lastSeq) ?? null;
147671
+ const stopped = lost ? null : lists?.stoppedOf(s.id, s.lastSeq) ?? null;
146956
147672
  if (stopped) stoppedBy.set(s.id, stopped);
146957
147673
  if (goesOn) {
146958
147674
  console.error(`[agent-host] ${s.id.slice(0, 8)}: an update of this machine ended its turn; continuing it`);
@@ -146974,8 +147690,8 @@ var SessionManager = class {
146974
147690
  }
146975
147691
  const cutIds = new Set(cutByUpdate.map((s) => s.id));
146976
147692
  this.updateCuts = [
146977
- ...cutByUpdate.map((s) => ({ sessionId: s.id, requester: requesterOf(s) ?? null, turn: true, stopped: stoppedBy.get(s.id) ?? null })),
146978
- ...[...stoppedBy].filter(([id3]) => !cutIds.has(id3)).map(([id3, stopped]) => ({ sessionId: id3, requester: null, turn: false, stopped }))
147693
+ ...cutByUpdate.map((s) => ({ sessionId: s.id, turn: true, stopped: stoppedBy.get(s.id) ?? null })),
147694
+ ...[...stoppedBy].filter(([id3]) => !cutIds.has(id3)).map(([id3, stopped]) => ({ sessionId: id3, turn: false, stopped }))
146979
147695
  ];
146980
147696
  this.cardRests.restore((id3) => !!keeperOpts.keptSessions?.has(id3) && this.meta.get(id3)?.state === "waiting_approval");
146981
147697
  for (const m of this.children.restoreReports(this.meta, goingOn)) this.quitNotices.push({ sessionId: m.sessionId, requester: m.callerSessionId, swapped: true });
@@ -147167,8 +147883,8 @@ var SessionManager = class {
147167
147883
  /** 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 */
147168
147884
  quitPersonWork = [];
147169
147885
  /**
147170
- * Sessions an update's stop left, to tell once the start is done (#1618, `continueCutTurns`): a turn it cut is continued (`turn`) and
147171
- * its requester told; a session at rest whose tasks it stopped is woken for the note alone (#1902). `stopped`: what else it ended there
147886
+ * Sessions an update's stop left, to tell once the start is done (#1618, `continueCutTurns`): a turn it cut is continued (`turn`; its end
147887
+ * says so, #1966); a session at rest whose tasks it stopped is woken for the note alone (#1902). `stopped`: what else it ended there
147172
147888
  */
147173
147889
  updateCuts = [];
147174
147890
  /**
@@ -147584,7 +148300,7 @@ var SessionManager = class {
147584
148300
  turnLostInSwap(m) {
147585
148301
  const change = { type: "state_change", sessionId: m.id, state: "idle", reason: "reattach_failed" };
147586
148302
  this.events.applyStateHint(change, m);
147587
- this.interruptions.markStoppedByQuit(m, true);
148303
+ this.interruptions.markTurnLost(m, true);
147588
148304
  this.store.sessions.upsertSession(m);
147589
148305
  this.emit(change);
147590
148306
  const turnBy = this.children.turnEnded(m.id, null, true);
@@ -147612,14 +148328,16 @@ var SessionManager = class {
147612
148328
  }
147613
148329
  m.agentVersion = k2.version ?? null;
147614
148330
  const r = await this.resumeSession(k2.sessionId, k2.process).catch((err) => ({ resumed: false, reason: err.message }));
147615
- if (!r.resumed && !this.lettingGo()) k2.process.kill("SIGTERM");
148331
+ let gone = Promise.resolve();
148332
+ if (!r.resumed && !this.lettingGo()) gone = this.endKept(k2.process);
147616
148333
  const adopted = r.resumed ? this.handles.get(k2.sessionId) : void 0;
147617
- if (adopted?.takesOverTurn === false && inTurn(m.state)) this.turnLostInSwap(m);
148334
+ if (adopted?.takesOverTurn === false && inTurn(m.state)) await this.continueLostTurn(m);
147618
148335
  if (this.children.queued.has(k2.sessionId)) this.queue.scheduleDrain(k2.sessionId);
147619
148336
  if (!r.resumed) {
147620
148337
  console.error(`[agent-host] could not re-attach ${k2.sessionId.slice(0, 8)}: ${r.reason ?? "unknown"}`);
147621
148338
  if (inTurn(m.state)) {
147622
- this.turnLostInSwap(m);
148339
+ await gone;
148340
+ await this.continueLostTurn(m);
147623
148341
  } else if (this.children.pausedReports.has(m.id)) {
147624
148342
  void this.reports.reportBackIfAwaited(m.id, { type: "state_change", sessionId: m.id, state: "idle" });
147625
148343
  }
@@ -147634,9 +148352,8 @@ var SessionManager = class {
147634
148352
  * Continues each turn a linked hub's update cut (#1618, owner 2026-10-26): the update no longer waits for the turns on a
147635
148353
  * machine, so the host it starts takes them up again, once. The session was left `working` by the start; it is woken
147636
148354
  * and given Centralu's note through Centralu's own channel (`deliverMail`: no message of the person's, so the turn stays
147637
- * 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
147638
- * 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
147639
- * (`tellAskerOfInterruption`). A session that cannot be continued (its tool cannot resume it, it is gone there) ends
148355
+ * whose it was and its report, if one is owed, is still that turn's end). Its requester is told nothing of the cut: the
148356
+ * report of the continued turn's end says so (#1966). A session that cannot be continued (its tool cannot resume it, it is gone there) ends
147640
148357
  * as a kept process that could not be re-attached does (`turnLostInSwap`): idle, marked stopped, its report or its
147641
148358
  * requester told. Each is taken once: the list is emptied first, and its id leaves the file once its Continue was sent
147642
148359
  * or its loss reported (`UpdateResumeLists.done`), not before, so a host stopped in between leaves it for the next.
@@ -147649,9 +148366,8 @@ var SessionManager = class {
147649
148366
  async continueCutTurns() {
147650
148367
  const cuts = this.updateCuts;
147651
148368
  this.updateCuts = [];
147652
- const continued = new Set(cuts.filter((c) => c.turn).map((c) => c.sessionId));
147653
148369
  await Promise.all(
147654
- cuts.map(async ({ sessionId, requester, turn, stopped }) => {
148370
+ cuts.map(async ({ sessionId, turn, stopped }) => {
147655
148371
  const m = this.meta.get(sessionId);
147656
148372
  if (this.disposing || this.handingOver) return;
147657
148373
  if (!m || turn && !inTurn(m.state)) {
@@ -147660,13 +148376,11 @@ var SessionManager = class {
147660
148376
  this.updateLists?.done(sessionId);
147661
148377
  return;
147662
148378
  }
148379
+ if (turn) this.children.noteContinuedCut(sessionId);
147663
148380
  try {
147664
- await this.events.deliverMail(
147665
- sessionId,
147666
- { text: updateNote(turn, stopped), quiet: false, line: turn ? UPDATE_CONTINUE_LINE : UPDATE_TASKS_LINE, label: "update", skipLine: () => this.disposing && this.stoppingForUpdate },
147667
- { leaving: () => this.handingOver || this.disposing, atRest: () => true }
147668
- );
148381
+ await this.sendUpdateNote(sessionId, turn, stopped);
147669
148382
  } catch (e) {
148383
+ this.children.dropContinuedCut(sessionId);
147670
148384
  if (this.disposing || this.handingOver || this.meta.get(sessionId) !== m) return;
147671
148385
  this.restartPolicy.continuingTurns.delete(sessionId);
147672
148386
  if (!turn) {
@@ -147683,11 +148397,50 @@ var SessionManager = class {
147683
148397
  if (this.disposing || this.handingOver) return;
147684
148398
  this.restartPolicy.continuingTurns.delete(sessionId);
147685
148399
  this.updateLists?.done(sessionId);
147686
- if (requester && !continued.has(requester)) void this.reports.tellCutByUpdate(sessionId, requester);
147687
148400
  })
147688
148401
  );
147689
148402
  this.tellQuitNotices(() => true);
147690
148403
  }
148404
+ /**
148405
+ * Centralu's note to the agent after an update or a swap stopped it (#1618, #1966): the Continue of a cut turn, or the tasks the stop ended
148406
+ * for a session at rest. The one door of the update's continue and of the swap's (`adoptKept`). Not sent once this host is leaving (a second
148407
+ * update's stop while the session wakes): the host after it sends the one
148408
+ */
148409
+ sendUpdateNote(sessionId, turn, stopped) {
148410
+ return this.events.deliverMail(
148411
+ sessionId,
148412
+ { text: updateNote(turn, stopped), quiet: false, line: turn ? UPDATE_CONTINUE_LINE : UPDATE_TASKS_LINE, label: "update", skipLine: () => this.disposing && this.stoppingForUpdate },
148413
+ { leaving: () => this.handingOver || this.disposing, atRest: () => true }
148414
+ );
148415
+ }
148416
+ /**
148417
+ * A turn a host swap cut that its tool cannot take up again (ACP reopens the conversation in a new process; a process that could not be
148418
+ * re-attached): continued like one an update cut, with the same Continue (#1966). Only a turn Centralu itself cut. One that cannot be
148419
+ * continued ends as it did: stopped, its report or its requester told (`turnLostInSwap`)
148420
+ */
148421
+ async continueLostTurn(m) {
148422
+ this.restartPolicy.continuingTurns.add(m.id);
148423
+ this.children.noteContinuedCut(m.id);
148424
+ try {
148425
+ await this.sendUpdateNote(m.id, true, null);
148426
+ } catch (e) {
148427
+ this.children.dropContinuedCut(m.id);
148428
+ if (this.disposing || this.handingOver || this.meta.get(m.id) !== m) return;
148429
+ console.error(`[agent-host] ${m.id.slice(0, 8)}: the turn a swap ended could not be continued (${errorMessage(e)})`);
148430
+ this.turnLostInSwap(m);
148431
+ } finally {
148432
+ this.restartPolicy.continuingTurns.delete(m.id);
148433
+ }
148434
+ }
148435
+ /** Stops a kept process nobody can address; resolves when it has exited, or after a bound (then it is killed) */
148436
+ endKept(p) {
148437
+ if (p.exitCode !== null || p.signalCode) return Promise.resolve();
148438
+ const exited = new Promise((r) => p.once("exit", () => r(true)));
148439
+ p.kill("SIGTERM");
148440
+ return raceTimeout(exited, this.keeperOpts.exitWaitMs ?? 5e3, false).then((done2) => {
148441
+ if (!done2) p.kill("SIGKILL");
148442
+ });
148443
+ }
147691
148444
  /** A turn the start took up that is not continued after all: the hello stops naming it (#1618) */
147692
148445
  notContinued(sessionId) {
147693
148446
  this.continuedAfterUpdate = this.continuedAfterUpdate.filter((c) => c.sessionId !== sessionId);
@@ -148910,7 +149663,7 @@ var MailPerson = class {
148910
149663
  /** A chain with its messages, which the person reads: what is returned is read for them. An old thread's id names the chain moved from it */
148911
149664
  thread(threadId, opts = {}) {
148912
149665
  const root3 = this.rootsOf(threadId)[0];
148913
- if (root3 === void 0) throw refuse5("unknown_message", `There is no mail ${threadId} kept.`);
149666
+ if (root3 === void 0) throw refuse4("unknown_message", `There is no mail ${threadId} kept.`);
148914
149667
  const all = this.shown(root3);
148915
149668
  let page;
148916
149669
  if (opts.after !== void 0) page = all.filter((s) => s.seq > (opts.after ?? 0)).slice(0, opts.all ? void 0 : THREAD_PAGE);
@@ -148952,7 +149705,7 @@ var MailPerson = class {
148952
149705
  const theirs = open6 ?? toPerson.at(-1);
148953
149706
  const mine = chain.findLast((m) => m.sender === PERSON && this.records.recipients(m.id).some((r) => this.here(r.recipient)));
148954
149707
  const target = theirs ?? mine;
148955
- if (!target) throw refuse5("unknown_message", `There is no message in ${threadId} to answer.`);
149708
+ if (!target) throw refuse4("unknown_message", `There is no message in ${threadId} to answer.`);
148956
149709
  const to = target === theirs ? [target.sender] : this.records.recipients(target.id).flatMap((r) => this.here(r.recipient) ? [r.recipient] : []);
148957
149710
  const sent = this.service.send(PERSON, { to, text: text3, replyTo: target.id, needsReply: kind === "question" });
148958
149711
  const at2 = this.shown(target.rootId).find((s) => s.m.id === sent.message.id)?.seq ?? 1;
@@ -148972,7 +149725,7 @@ var MailPerson = class {
148972
149725
  close(threadId, outcome = "finished") {
148973
149726
  const roots = this.rootsOf(threadId);
148974
149727
  const root3 = roots[0];
148975
- if (root3 === void 0) throw refuse5("unknown_message", `There is no mail ${threadId} kept.`);
149728
+ if (root3 === void 0) throw refuse4("unknown_message", `There is no mail ${threadId} kept.`);
148976
149729
  const now = Date.now();
148977
149730
  for (const r of roots) {
148978
149731
  const chain = this.chainOf(r);
@@ -149010,7 +149763,7 @@ var MailPerson = class {
149010
149763
  if (action === "dismiss") return { changed: this.service.dismissHeld(recipient, messageIds, PERSON_HOLDS) };
149011
149764
  const projects = new Set(messageIds.flatMap((id3) => this.records.recipient(id3, recipient)?.held === "policy" ? this.records.message(id3)?.senderProject ?? [] : []));
149012
149765
  const still = [...projects].find((p) => this.host.untrusted(p));
149013
- if (still !== void 0) throw refuse5("untrusted_project", `${this.host.projectLabel(still)} is not trusted: what its sessions send out of it waits until you trust the project, and then goes in order.`);
149766
+ if (still !== void 0) throw refuse4("untrusted_project", `${this.host.projectLabel(still)} is not trusted: what its sessions send out of it waits until you trust the project, and then goes in order.`);
149014
149767
  const policy = messageIds.filter((id3) => this.records.recipient(id3, recipient)?.held === "policy");
149015
149768
  for (const p of projects) this.trusted(p);
149016
149769
  const letGo = policy.filter((id3) => this.records.recipient(id3, recipient)?.held !== "policy").length;
@@ -149244,151 +149997,6 @@ function askedBy(host, turns, orchestratorId) {
149244
149997
  return { messageId: turn.messageId, sessionId: asker.sessionId, turn: turn.turnSeq, ...askerFacts(host, asker.sessionId), cites: host.cites(turn.messageId) };
149245
149998
  }
149246
149999
 
149247
- // packages/agent-host/src/mail/delegation.ts
149248
- function delegationRecorder(mail, turns) {
149249
- return (callerId, childId, origin, task) => {
149250
- const written = mail.task(mail.sessionAddress(callerId), mail.sessionAddress(childId), { text: task, origin: delegationOrigin(origin), delivered: true });
149251
- turns?.attach(childId, written.id, callerId);
149252
- };
149253
- }
149254
- function delegationOrigin(origin) {
149255
- return origin === "start_agent" || origin === "agent_session" || origin === "send_to_session" || origin === "create_session" || origin === "ask_project" ? origin : "send_to_session";
149256
- }
149257
- function personWorkRecorder(mail) {
149258
- return (sessionId, text3, fromScript) => {
149259
- mail.task(PERSON, mail.sessionAddress(sessionId), { text: text3, origin: fromScript ? "script" : "create_session", delivered: true });
149260
- };
149261
- }
149262
- function personOwes(mail, records) {
149263
- return (sessionId) => records.newestTask(PERSON, mail.sessionAddress(sessionId)) !== void 0;
149264
- }
149265
- function personReportRecorder(mail, records, waitingOn = () => 0) {
149266
- return (sessionId, draft) => {
149267
- const task = records.newestTask(PERSON, mail.sessionAddress(sessionId));
149268
- if (!task) return false;
149269
- const owed = draft.then === "review" && draft.outcome === "finished" ? waitingOn(sessionId) : 0;
149270
- try {
149271
- mail.report(task.id, turnReport(owed > 0 ? { ...draft, outcome: "paused", then: "running" } : draft, owed));
149272
- return true;
149273
- } catch (e) {
149274
- if (refusalReason(e) !== void 0) return false;
149275
- throw e;
149276
- }
149277
- };
149278
- }
149279
- var ANSWER_KEPT = 600;
149280
- function answerRecorder(mail, records, turns, waitingOn = () => 0) {
149281
- return (callerId, childId, how) => {
149282
- const owner = mail.sessionAddress(callerId);
149283
- const child = mail.sessionAddress(childId);
149284
- const now = Date.now();
149285
- const reports = records.unreadFor(owner).filter((p) => p.message.kind === "report" && reportOnTaskTo(records, p.message, owner, child)).map((p) => p.message.id);
149286
- records.markDelivered(owner, reports, now, null);
149287
- records.markRead(owner, reports, now);
149288
- const task = records.newestTask(owner, child);
149289
- if (!how || !task) return;
149290
- const read = `read_session(sessionId="${childId}")`;
149291
- const waiting = how.ok ? waitingOn(callerId, childId) : 0;
149292
- const startedAt = turns?.latest(childId)?.startedAt;
149293
- const written = mail.report(task.id, {
149294
- ...startedAt !== void 0 ? { startedAt } : {},
149295
- ...waiting > 0 ? { outcome: "waiting", waitingOn: waiting } : { outcome: how.ok ? "finished" : "failed" },
149296
- body: how.ok ? `[Centralu] The assigned work finished.
149297
- id: ${childId}
149298
-
149299
- Its last response is that session's own words, so it is not repeated here: ${read} reads it.` : `[Centralu] The assigned work did not finish.
149300
- id: ${childId}
149301
-
149302
- Its tool's error, by kind: ${how.error}. Its words are its tool's, not Centralu's, so they are not repeated here: ${read} shows them.`,
149303
- ...how.ok ? { answer: how.text.length > ANSWER_KEPT ? `${how.text.slice(0, ANSWER_KEPT)}\u2026` : how.text } : { error: how.error }
149304
- });
149305
- if ("message" in written) {
149306
- records.markDelivered(owner, [written.message.id], now, null);
149307
- records.markRead(owner, [written.message.id], now);
149308
- }
149309
- };
149310
- }
149311
- function reportOnTaskTo(records, report, owner, child) {
149312
- const task = report.replyTo === null ? void 0 : records.message(report.replyTo);
149313
- return task?.kind === "task" && task.sender === owner && records.recipient(task.id, child) !== void 0;
149314
- }
149315
- function turnRecorder(mail, records, turns) {
149316
- return (sessionId, turn) => {
149317
- const child = mail.sessionAddress(sessionId);
149318
- const taskId = turn.ownerId && (turn.by === "work" || turn.added) ? records.newestTask(mail.sessionAddress(turn.ownerId), child)?.id ?? null : null;
149319
- const personWork = () => turn.ownerId ? null : records.newestTask(PERSON, child)?.id ?? null;
149320
- const record2 = (by, messageId, owner) => turn.again ? turns.restate(sessionId, by, messageId, owner) : turns.beginOther(sessionId, by, messageId, owner);
149321
- if (turn.by === "person") record2("person", null);
149322
- else if (turn.added) record2("person", taskId, turn.ownerId);
149323
- else if (turn.source === "report") record2("report", null);
149324
- else if (turn.source === "app") record2("app", null);
149325
- else {
149326
- const person = taskId === null ? personWork() : null;
149327
- record2(turn.cli && person === null ? "cli" : "caller", taskId ?? person, turn.ownerId);
149328
- }
149329
- };
149330
- }
149331
- function outcomeOf2(draft) {
149332
- if (draft.then === "dropped") return { outcome: "stopped", reason: "taken_over" };
149333
- const word = draft.outcome;
149334
- const outcome = word === "finished" || word === "settled" ? "finished" : word === "failed" || word === "limit_reached" ? "failed" : "stopped";
149335
- return { outcome, ...word !== outcome ? { reason: word } : {} };
149336
- }
149337
- function turnReport(draft, waitingOn) {
149338
- const base2 = { body: draft.body, ...draft.answer ? { answer: draft.answer } : {}, ...draft.error ? { error: draft.error } : {}, ...draft.personNote ? { personNote: true } : {} };
149339
- if (draft.then === "running" && draft.paused) return { ...base2, outcome: "paused", paused: draft.paused };
149340
- if (draft.then === "running") return { ...base2, outcome: "waiting", waitingOn: Math.max(1, waitingOn) };
149341
- return { ...base2, ...outcomeOf2(draft) };
149342
- }
149343
- function askedReportBody(draft) {
149344
- return draft.outcome === "finished" ? "[Centralu] The orchestrator ended its turn on your request. What it sent you in reply is its answer; if it sent nothing, it had none to give. Its other words in that turn are not passed on." : `[Centralu] The orchestrator's turn on your request did not finish (${draft.outcome}). What it sent you in reply, if anything, has reached you. Ask again, or ask the person.`;
149345
- }
149346
- function reportedWork(records, turns, owner, child, childId) {
149347
- const turnWork = turns.latest(childId)?.messageId;
149348
- const work = turnWork == null ? void 0 : records.message(turnWork);
149349
- if (work && work.sender === owner && records.recipient(work.id, child)) {
149350
- if (work.kind === "task") return { work, request: false };
149351
- if (work.kind === "message") return { work, request: true };
149352
- }
149353
- const task = records.newestTask(owner, child);
149354
- return task && { work: task, request: false };
149355
- }
149356
- function reportRecorder(mail, records, turns, waitingOn = () => 0) {
149357
- return (ownerId, childId, draft) => {
149358
- const owner = mail.sessionAddress(ownerId);
149359
- const child = mail.sessionAddress(childId);
149360
- const found = reportedWork(records, turns, owner, child, childId);
149361
- if (!found) return "none";
149362
- if (draft.archiving) return "closed";
149363
- if (draft.outcome === "resumed") return "none";
149364
- const { work, request } = found;
149365
- let written;
149366
- try {
149367
- const { answer: _answer, ...unanswered2 } = draft;
149368
- const startedAt = turns.latest(childId)?.startedAt;
149369
- const report = request ? { ...turnReport(unanswered2, 0), body: askedReportBody(draft) } : turnReport(draft, waitingOn(ownerId, childId));
149370
- written = mail.report(work.id, startedAt === void 0 ? report : { ...report, startedAt });
149371
- } catch (e) {
149372
- if (refusalReason(e) !== void 0) return "none";
149373
- throw e;
149374
- }
149375
- const seq = turns.latest(childId)?.seq;
149376
- if (request && seq !== void 0 && draft.then !== "running") mail.requestTurnEnded(child, owner, seq);
149377
- if ("settledAlready" in written || written.duplicate) return "closed";
149378
- if (draft.delivery === "wake") return "mailed";
149379
- if (!written.first && !draft.ownerInTurn) return "closed";
149380
- const reportId = written.message.id;
149381
- return {
149382
- keep: true,
149383
- settle: () => {
149384
- const now = Date.now();
149385
- records.markDelivered(owner, [reportId], now, null);
149386
- records.markRead(owner, [reportId], now);
149387
- }
149388
- };
149389
- };
149390
- }
149391
-
149392
150000
  // packages/agent-host/src/mail/messages.ts
149393
150001
  import { createHash as createHash13 } from "node:crypto";
149394
150002
  var CITE_MS = 30 * 6e4;
@@ -149446,18 +150054,18 @@ var MessageService = class {
149446
150054
  */
149447
150055
  send(sender, input) {
149448
150056
  const from = parseAddress(sender);
149449
- if (!from || from.kind === "centralu") throw refuse5("not_allowed", "Centralu sends with notice and report");
150057
+ if (!from || from.kind === "centralu") throw refuse4("not_allowed", "Centralu sends with notice and report");
149450
150058
  const cls = from.kind;
149451
150059
  const text3 = this.checkText(cls, input.text);
149452
- if (input.urgent === true && cls !== "session" && cls !== "person") throw refuse5("field_not_allowed", "only a session or the person marks mail urgent");
150060
+ if (input.urgent === true && cls !== "session" && cls !== "person") throw refuse4("field_not_allowed", "only a session or the person marks mail urgent");
149453
150061
  const target = input.replyTo === void 0 ? void 0 : this.readable(sender, input.replyTo);
149454
150062
  let to = [...new Set(input.to)];
149455
150063
  if (to.length === 0) {
149456
- if (!target) throw refuse5("bad_value", "a message needs at least one recipient");
149457
- if (target.sender === sender) throw refuse5("own_message", REFUSAL_TEXT.own_message(target.id));
150064
+ if (!target) throw refuse4("bad_value", "a message needs at least one recipient");
150065
+ if (target.sender === sender) throw refuse4("own_message", REFUSAL_TEXT.own_message(target.id));
149458
150066
  to = [target.sender];
149459
150067
  }
149460
- if (to.length > MAX_RECIPIENTS) throw refuse5("recipients", REFUSAL_TEXT.recipients());
150068
+ if (to.length > MAX_RECIPIENTS) throw refuse4("recipients", REFUSAL_TEXT.recipients());
149461
150069
  const causeId = this.causeOf(sender);
149462
150070
  const cause2 = causeId === null ? null : this.records.message(causeId) ?? null;
149463
150071
  const clarifies = cls === "session" && cause2?.kind === "task" && to.length === 1 && to[0] === cause2.sender ? cause2.id : void 0;
@@ -149466,10 +150074,10 @@ var MessageService = class {
149466
150074
  const requests2 = to.map((r) => this.isRequest({ sender, kind: "message", replyTo: target?.id ?? null, facts: { clarifies } }, r));
149467
150075
  for (const [i, r] of to.entries()) {
149468
150076
  const party = parseAddress(r);
149469
- if (!party) throw refuse5("bad_address", `Not a mail address: ${JSON.stringify(r)}`);
149470
- if (r === sender) throw refuse5("bad_value", "a message is for someone else");
150077
+ if (!party) throw refuse4("bad_address", `Not a mail address: ${JSON.stringify(r)}`);
150078
+ if (r === sender) throw refuse4("bad_value", "a message is for someone else");
149471
150079
  const why2 = this.reach.mayWrite(sender, r, requests2[i]);
149472
- if (why2 !== null) throw refuse5(why2, this.refusalText(why2, r));
150080
+ if (why2 !== null) throw refuse4(why2, this.refusalText(why2, r));
149473
150081
  }
149474
150082
  return this.run(() => {
149475
150083
  const now = this.now();
@@ -149514,9 +150122,9 @@ var MessageService = class {
149514
150122
  /** Work a delegation tool gave: a task from `sender` to `recipient`, whose turn Centralu reports (D2, D7). Written delivered when the old path delivered its words */
149515
150123
  task(sender, recipient, input) {
149516
150124
  const from = parseAddress(sender);
149517
- if (!from || from.kind === "centralu") throw refuse5("not_allowed", "Centralu sends with notice and report");
150125
+ if (!from || from.kind === "centralu") throw refuse4("not_allowed", "Centralu sends with notice and report");
149518
150126
  const text3 = cutTask(input.text, bodyLimit(from.kind));
149519
- if (text3 === "") throw refuse5("empty", "a task says something");
150127
+ if (text3 === "") throw refuse4("empty", "a task says something");
149520
150128
  return this.run(() => {
149521
150129
  const now = this.now();
149522
150130
  return this.put(
@@ -149529,12 +150137,12 @@ var MessageService = class {
149529
150137
  refused(callerId, input) {
149530
150138
  const to = this.sessionAddress(callerId);
149531
150139
  const now = this.now();
149532
- const firstLine3 = oneLineText(input.firstLine, 120);
150140
+ const firstLine4 = oneLineText(input.firstLine, 120);
149533
150141
  this.run(
149534
150142
  () => this.centralu(to, {
149535
150143
  kind: "notice",
149536
150144
  body: REFUSAL_TEXT.untrusted_project(this.reach.projectLabel(input.projectId), input.target),
149537
- facts: { refused: { ...input, firstLine: firstLine3 } },
150145
+ facts: { refused: { ...input, firstLine: firstLine4 } },
149538
150146
  dedupeKey: `refused:${to}:${hash16([input.tool, input.target, input.firstLine])}:${Math.floor(now / MINUTE_MS)}`,
149539
150147
  delivered: true
149540
150148
  })
@@ -149549,7 +150157,7 @@ var MessageService = class {
149549
150157
  return this.run(() => {
149550
150158
  const task = this.records.message(taskId);
149551
150159
  const row = task && (task.kind === "task" || task.kind === "message") ? this.records.recipients(taskId).find((r) => task.kind === "task" || this.isRequest(task, r.recipient)) : void 0;
149552
- if (!task || !row) throw refuse5("unknown_message", REFUSAL_TEXT.unknown_message(taskId));
150160
+ if (!task || !row) throw refuse4("unknown_message", REFUSAL_TEXT.unknown_message(taskId));
149553
150161
  if (row.settled !== null && task.kind === "task") return { settledAlready: true };
149554
150162
  const now = this.now();
149555
150163
  const pause = input.outcome === "paused" || input.outcome === "waiting";
@@ -149603,11 +150211,11 @@ var MessageService = class {
149603
150211
  done(by, id3) {
149604
150212
  return this.run(() => {
149605
150213
  const m = this.records.message(id3);
149606
- if (!m) throw refuse5("unknown_message", REFUSAL_TEXT.unknown_message(id3));
150214
+ if (!m) throw refuse4("unknown_message", REFUSAL_TEXT.unknown_message(id3));
149607
150215
  const mine = m.sender === by;
149608
150216
  const row = mine ? void 0 : this.records.recipient(id3, by);
149609
- if (!mine && !row) throw refuse5("not_yours", REFUSAL_TEXT.not_yours(id3));
149610
- if (!mine && this.isRequest(m, by) && m.sender !== this.opts.turn?.(by)?.asker) throw refuse5("not_allowed", REFUSAL_TEXT.withheld(id3));
150217
+ if (!mine && !row) throw refuse4("not_yours", REFUSAL_TEXT.not_yours(id3));
150218
+ if (!mine && this.isRequest(m, by) && m.sender !== this.opts.turn?.(by)?.asker) throw refuse4("not_allowed", REFUSAL_TEXT.withheld(id3));
149611
150219
  if (!m.needsReply && (m.kind !== "task" || !mine)) return { settled: 0, how: "unwatched" };
149612
150220
  if (mine) {
149613
150221
  const n = this.records.settle(id3, "all", "withdrawn", this.now());
@@ -149642,7 +150250,7 @@ var MessageService = class {
149642
150250
  }
149643
150251
  const id3 = opts.id ?? opts.before ?? "";
149644
150252
  const base2 = opts.id === void 0 ? this.records.message(id3) : this.readable(reader, id3);
149645
- if (!base2) throw refuse5("unknown_message", REFUSAL_TEXT.unknown_message(id3));
150253
+ if (!base2) throw refuse4("unknown_message", REFUSAL_TEXT.unknown_message(id3));
149646
150254
  const chain = this.records.chain(id3, READ_CHAIN);
149647
150255
  const oldest = chain.at(-1) ?? base2;
149648
150256
  const missing = oldest.replyTo !== null && chain.length < READ_CHAIN && this.records.message(oldest.replyTo) === void 0;
@@ -150008,7 +150616,7 @@ var MessageService = class {
150008
150616
  rows3
150009
150617
  );
150010
150618
  } catch (e) {
150011
- if (e instanceof MailTargetMissing) throw refuse5("unknown_message", REFUSAL_TEXT.unknown_message(e.replyTo));
150619
+ if (e instanceof MailTargetMissing) throw refuse4("unknown_message", REFUSAL_TEXT.unknown_message(e.replyTo));
150012
150620
  throw e;
150013
150621
  }
150014
150622
  if (!stored.duplicate) this.changed(stored, rows3.map((r) => r.recipient));
@@ -150029,9 +150637,9 @@ var MessageService = class {
150029
150637
  );
150030
150638
  }
150031
150639
  checkText(cls, text3) {
150032
- if (typeof text3 !== "string") throw refuse5("bad_value", "a body is text");
150033
- if (text3.trim() === "") throw refuse5("empty", "a post says something");
150034
- if (text3.length > bodyLimit(cls)) throw refuse5("too_long", `a body from ${cls === "app" ? "an app" : "this sender"} is at most ${bodyLimit(cls)} characters`);
150640
+ if (typeof text3 !== "string") throw refuse4("bad_value", "a body is text");
150641
+ if (text3.trim() === "") throw refuse4("empty", "a post says something");
150642
+ if (text3.length > bodyLimit(cls)) throw refuse4("too_long", `a body from ${cls === "app" ? "an app" : "this sender"} is at most ${bodyLimit(cls)} characters`);
150035
150643
  return text3;
150036
150644
  }
150037
150645
  refusalText(why2, to) {
@@ -150048,7 +150656,7 @@ var MessageService = class {
150048
150656
  readable(reader, id3) {
150049
150657
  const m = this.records.message(id3);
150050
150658
  const row = m && m.sender !== reader ? this.records.recipient(id3, reader) : void 0;
150051
- if (!m || m.sender !== reader && (!row || isHidden(row))) throw refuse5("unknown_message", REFUSAL_TEXT.unknown_message(id3));
150659
+ if (!m || m.sender !== reader && (!row || isHidden(row))) throw refuse4("unknown_message", REFUSAL_TEXT.unknown_message(id3));
150052
150660
  return m;
150053
150661
  }
150054
150662
  /** Whether `target` is a clarification of a task `owner` gave, and the answer goes to the one who asked (§9.7) */
@@ -150074,18 +150682,18 @@ var MessageService = class {
150074
150682
  /** The guards of a send, over the store so a restart does not reset them (§7.5) */
150075
150683
  guards(sender, cls, to, text3, target, requests2, now) {
150076
150684
  for (const r of to) {
150077
- if ((cls === "session" || cls === "app") && this.records.sameBody(sender, r, text3, now - REPEAT_WINDOW_MS)) throw refuse5("repeat", REFUSAL_TEXT.repeat(this.reach.labelOf(r)));
150685
+ if ((cls === "session" || cls === "app") && this.records.sameBody(sender, r, text3, now - REPEAT_WINDOW_MS)) throw refuse4("repeat", REFUSAL_TEXT.repeat(this.reach.labelOf(r)));
150078
150686
  }
150079
150687
  const limit = postsPerMinute(cls);
150080
150688
  if (limit !== Infinity && this.records.postsSince(sender, now - MINUTE_MS) >= limit) {
150081
- throw refuse5("rate_limited", `at most ${limit} posts a minute from ${cls === "app" ? "an app" : "a session"}: wait a minute`);
150689
+ throw refuse4("rate_limited", `at most ${limit} posts a minute from ${cls === "app" ? "an app" : "a session"}: wait a minute`);
150082
150690
  }
150083
150691
  for (const [i, r] of to.entries()) {
150084
- if (target === void 0 && cls === "session" && parseAddress(r)?.kind === "session" && this.records.newSince(sender, r, now - HOUR_MS3) >= PAIR_NEW_PER_HOUR) {
150085
- throw refuse5("pair_rate", REFUSAL_TEXT.pair_rate(this.reach.labelOf(r)));
150692
+ if (target === void 0 && cls === "session" && parseAddress(r)?.kind === "session" && this.records.newSince(sender, r, now - HOUR_MS) >= PAIR_NEW_PER_HOUR) {
150693
+ throw refuse4("pair_rate", REFUSAL_TEXT.pair_rate(this.reach.labelOf(r)));
150086
150694
  }
150087
150695
  if (requests2[i]) {
150088
- if (this.records.requestsSince(this.reach.askGroupMembers(sender), r, now - HOUR_MS3) >= ASKS_PER_HOUR) throw refuse5("ask_rate", REFUSAL_TEXT.ask_rate());
150696
+ if (this.records.requestsSince(this.reach.askGroupMembers(sender), r, now - HOUR_MS) >= ASKS_PER_HOUR) throw refuse4("ask_rate", REFUSAL_TEXT.ask_rate());
150089
150697
  }
150090
150698
  }
150091
150699
  }
@@ -150197,7 +150805,8 @@ var MessageService = class {
150197
150805
  ...answered?.sender === recipient ? { answers: answered.id } : {},
150198
150806
  ...typeof m.facts.clarifies === "string" ? { clarifies: m.facts.clarifies } : {},
150199
150807
  ...typeof about === "string" && parseAddress(about) ? { about } : {},
150200
- ...m.facts.outcome ? { outcome: m.facts.outcome } : {},
150808
+ // The stored word `finished` is the host's knowledge that a turn ended, not that the work is done (#1902): the model reads it as that
150809
+ ...m.facts.outcome ? { outcome: m.facts.outcome === "finished" ? "turn_ended" : m.facts.outcome } : {},
150201
150810
  ...m.facts.error ? { error: m.facts.error } : {},
150202
150811
  ...m.facts.reason ? { reason: m.facts.reason } : {},
150203
150812
  ...m.facts.paused !== void 0 ? { paused: m.facts.paused } : {},
@@ -150319,7 +150928,7 @@ var MailTool = class {
150319
150928
  run(sessionId, args) {
150320
150929
  try {
150321
150930
  const me2 = this.host.caller(sessionId);
150322
- if (!me2) throw refuse5("gone", "this session is not here");
150931
+ if (!me2) throw refuse4("gone", "this session is not here");
150323
150932
  return { text: this.dispatch(sessionId, me2, args, mailCalls(this.host.mailArgs(sessionId), this.host.form(sessionId))) };
150324
150933
  } catch (e) {
150325
150934
  const reason = refusalReason(e);
@@ -150346,7 +150955,7 @@ var MailTool = class {
150346
150955
  }
150347
150956
  case "update":
150348
150957
  case "close":
150349
- throw refuse5(
150958
+ throw refuse4(
150350
150959
  "removed",
150351
150960
  `Centralu's mail has no threads any more, so "${a.action}" is gone. Answer a message with ${calls.answer("<message id>")}, and mark one that needs nothing with ${calls.done("<message id>")}.`
150352
150961
  );
@@ -150361,7 +150970,7 @@ var MailTool = class {
150361
150970
  const id3 = thread.trim();
150362
150971
  if (parseMailId(id3)?.kind !== "thread") return id3;
150363
150972
  const newest = this.records.newestLegacyTo(id3, me2);
150364
- if (!newest) throw refuse5("unknown_message", `there is no message of thread ${id3} you were sent; check the id with mail(action="inbox").`);
150973
+ if (!newest) throw refuse4("unknown_message", `there is no message of thread ${id3} you were sent; check the id with mail(action="inbox").`);
150365
150974
  return newest.id;
150366
150975
  }
150367
150976
  /**
@@ -150373,14 +150982,14 @@ var MailTool = class {
150373
150982
  if (t === PERSON) return { address: PERSON };
150374
150983
  if (t.toLowerCase() === "orchestrator") {
150375
150984
  const id3 = this.host.orchestratorId();
150376
- if (!id3) throw refuse5("no_mailbox", "there is no orchestrator to ask yet: write to the person instead");
150985
+ if (!id3) throw refuse4("no_mailbox", "there is no orchestrator to ask yet: write to the person instead");
150377
150986
  return { address: this.service.sessionAddress(id3), via: "orchestrator" };
150378
150987
  }
150379
150988
  if (t.toLowerCase() === "parent") {
150380
- if (!me2.askedBy) throw refuse5("unknown", 'this session was not started by another session; write to "orchestrator" or "person".');
150989
+ if (!me2.askedBy) throw refuse4("unknown", 'this session was not started by another session; write to "orchestrator" or "person".');
150381
150990
  return { address: this.service.sessionAddress(me2.askedBy) };
150382
150991
  }
150383
- if (t === sessionId) throw refuse5("bad_value", "a message is for someone else");
150992
+ if (t === sessionId) throw refuse4("bad_value", "a message is for someone else");
150384
150993
  return { address: t.includes(":") ? t : this.service.sessionAddress(t) };
150385
150994
  }
150386
150995
  /** `send`'s and `reply`'s answer (S4's lines, in this tool's calls) */
@@ -150639,7 +151248,7 @@ var MailTurns = class {
150639
151248
  else if (outcome === "acted") this.empty.delete(sessionId);
150640
151249
  }
150641
151250
  recent(sessionId) {
150642
- const since = this.now() - HOUR_MS3;
151251
+ const since = this.now() - HOUR_MS;
150643
151252
  return (this.tokens.get(sessionId) ?? []).filter((t) => t.at > since);
150644
151253
  }
150645
151254
  /** Tokens the session's mail-started turns read in the last hour, the open one included */
@@ -150664,7 +151273,7 @@ var MailTurns = class {
150664
151273
  * counted since the latest of an hour ago, the person's last word to either (a turn of theirs or a message), and the pair's Resume (§7.5)
150665
151274
  */
150666
151275
  pairLoopSpent(a, b2) {
150667
- const since = Math.max(this.now() - HOUR_MS3, this.lastPersonWord(a), this.lastPersonWord(b2), this.pairFrom.get(pairKey(a, b2)) ?? 0);
151276
+ const since = Math.max(this.now() - HOUR_MS, this.lastPersonWord(a), this.lastPersonWord(b2), this.pairFrom.get(pairKey(a, b2)) ?? 0);
150668
151277
  return this.records.pairTurnsSince(a, b2, since) > this.o.turnsPerHour;
150669
151278
  }
150670
151279
  /** The person pressed Resume on a pair that was paused: its turns are counted from now */
@@ -150867,8 +151476,8 @@ function refusesDelegation(service, reach, callerId, target, call2) {
150867
151476
  }
150868
151477
 
150869
151478
  // packages/agent-host/src/sessions/memory-gate.ts
150870
- import { execFile as execFile11 } from "node:child_process";
150871
- import { readFile as readFile8 } from "node:fs/promises";
151479
+ import { execFile as execFile12 } from "node:child_process";
151480
+ import { readFile as readFile9 } from "node:fs/promises";
150872
151481
  import { freemem } from "node:os";
150873
151482
  import { promisify as promisify10 } from "node:util";
150874
151483
  var MEMORY_FLOOR_BYTES = 2 * 1024 ** 3;
@@ -150890,14 +151499,14 @@ function parseMemInfo(text3) {
150890
151499
  const m = /^MemAvailable:\s+(\d+)\s*kB\s*$/m.exec(text3);
150891
151500
  return m ? Number(m[1]) * 1024 : null;
150892
151501
  }
150893
- var run = promisify10(execFile11);
151502
+ var run = promisify10(execFile12);
150894
151503
  async function readAvailableMemory(deps = {}) {
150895
151504
  const source = deps.source ?? availableMemorySource();
150896
151505
  const free = deps.free ?? freemem;
150897
151506
  if (source.kind === "freemem") return free();
150898
151507
  const what = source.kind === "command" ? source.file : source.path;
150899
151508
  try {
150900
- const value = source.kind === "command" ? parseVmStat((await (deps.exec ?? ((f, a) => run(f, a, { timeout: 5e3, windowsHide: true })))(source.file, source.args)).stdout) : parseMemInfo(await (deps.readText ?? ((p) => readFile8(p, "utf8")))(source.path));
151509
+ const value = source.kind === "command" ? parseVmStat((await (deps.exec ?? ((f, a) => run(f, a, { timeout: 5e3, windowsHide: true })))(source.file, source.args)).stdout) : parseMemInfo(await (deps.readText ?? ((p) => readFile9(p, "utf8")))(source.path));
150901
151510
  if (value !== null) return value;
150902
151511
  console.error(`[memory-gate] could not parse ${what}; letting agent starts through`);
150903
151512
  } catch (e) {
@@ -150991,126 +151600,12 @@ var MemoryGate = class {
150991
151600
  // packages/agent-host/src/rpc.ts
150992
151601
  import { relative as relative8 } from "node:path";
150993
151602
 
150994
- // packages/agent-host/src/themes.ts
150995
- import { randomBytes as randomBytes11 } from "node:crypto";
150996
- import { watch as watch4 } from "node:fs";
150997
- import { mkdir as mkdir9, readdir as readdir15, readFile as readFile9, stat as stat11 } from "node:fs/promises";
150998
- import { basename as basename14, join as join55 } from "node:path";
150999
- var MAX_FILE_BYTES = 256 * 1024;
151000
- var SETTLE_MS = 100;
151001
- var ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,59}$/;
151002
- var ThemeFiles = class {
151003
- constructor(dir, onChange) {
151004
- this.dir = dir;
151005
- this.onChange = onChange;
151006
- }
151007
- dir;
151008
- onChange;
151009
- watcher = null;
151010
- timer = null;
151011
- /** Makes the folder, writes the schema next to the themes, and starts watching */
151012
- async start() {
151013
- await mkdir9(this.dir, { recursive: true });
151014
- await this.writeSchema();
151015
- try {
151016
- this.watcher = watch4(this.dir, (_event, name) => {
151017
- if (name && !String(name).endsWith(".json")) return;
151018
- if (name === THEME_SCHEMA_FILE) return;
151019
- if (this.timer) clearTimeout(this.timer);
151020
- this.timer = setTimeout(() => {
151021
- this.timer = null;
151022
- this.onChange();
151023
- }, SETTLE_MS);
151024
- });
151025
- this.watcher.on("error", () => this.stopWatching());
151026
- } catch {
151027
- }
151028
- }
151029
- close() {
151030
- this.stopWatching();
151031
- if (this.timer) clearTimeout(this.timer);
151032
- this.timer = null;
151033
- }
151034
- stopWatching() {
151035
- this.watcher?.close();
151036
- this.watcher = null;
151037
- }
151038
- /**
151039
- * The schema an editor uses for completion, written only when it differs — a build that adds
151040
- * a token updates it, and an unchanged one does not touch the folder (or wake the watcher).
151041
- */
151042
- async writeSchema() {
151043
- const path2 = join55(this.dir, THEME_SCHEMA_FILE);
151044
- const text3 = `${JSON.stringify(themeFileJsonSchema(), null, 2)}
151045
- `;
151046
- const current = await readFile9(path2, "utf8").catch(() => null);
151047
- if (current !== text3) writeFileWhole(realOs().files, path2, text3);
151048
- }
151049
- /** Every `<id>.json` in the folder, read and checked. A file that cannot be read is listed with the reason. */
151050
- async list() {
151051
- const names = await readdir15(this.dir).catch(() => []);
151052
- const entries = [];
151053
- const ids = names.filter((name) => name.endsWith(".json") && name !== THEME_SCHEMA_FILE).map((name) => name.slice(0, -".json".length)).filter((id3) => ID_PATTERN.test(id3)).sort();
151054
- for (const id3 of ids) entries.push(await this.read(id3));
151055
- return entries;
151056
- }
151057
- async read(id3) {
151058
- const path2 = this.pathOf(id3);
151059
- try {
151060
- const info = await stat11(path2);
151061
- if (info.size > MAX_FILE_BYTES) {
151062
- return { id: id3, path: path2, name: id3, base: "dark", tokens: {}, problems: ["The file is too large to be a theme."], broken: true };
151063
- }
151064
- return parseThemeFile(id3, path2, await readFile9(path2, "utf8"));
151065
- } catch (e) {
151066
- return { id: id3, path: path2, name: id3, base: "dark", tokens: {}, problems: [`Could not read the file: ${e.message}`], broken: true };
151067
- }
151068
- }
151069
- /** Writes a theme. `id` null makes a new file named after the theme (`My theme` → `my-theme.json`). */
151070
- async save(id3, content) {
151071
- const target = id3 ?? await this.freeId(content.name);
151072
- await mkdir9(this.dir, { recursive: true });
151073
- writeFileWhole(realOs().files, this.pathOf(target), formatThemeFile(content));
151074
- return this.read(target);
151075
- }
151076
- /** Copies a file from elsewhere in, under a fresh id if its name is taken. Refuses what does not read as a theme. */
151077
- async importFrom(source) {
151078
- const info = await stat11(source);
151079
- if (!info.isFile()) throw rpcError("That is not a file.");
151080
- if (info.size > MAX_FILE_BYTES) throw rpcError("The file is too large to be a theme.");
151081
- const text3 = await readFile9(source, "utf8");
151082
- const parsed = parseThemeFile("import", source, text3);
151083
- if (parsed.broken) throw rpcError(parsed.problems[0] ?? "Not a theme file.");
151084
- const fromName = basename14(source).replace(/\.json$/i, "");
151085
- const id3 = await this.freeId(parsed.name !== "import" ? parsed.name : fromName);
151086
- writeFileWhole(realOs().files, this.pathOf(id3), text3);
151087
- return this.read(id3);
151088
- }
151089
- /** The absolute path for an id, for the shell to trash or reveal. Refuses anything that is not an id. */
151090
- pathOf(id3) {
151091
- if (!ID_PATTERN.test(id3)) throw rpcError(`Not a theme id: ${id3}`);
151092
- return join55(this.dir, `${id3}.json`);
151093
- }
151094
- async freeId(name) {
151095
- const base2 = name.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "theme";
151096
- for (let n = 1; n < 1e3; n++) {
151097
- const id3 = n === 1 ? base2 : `${base2}-${n}`;
151098
- const taken = await stat11(this.pathOf(id3)).then(
151099
- () => true,
151100
- () => false
151101
- );
151102
- if (!taken) return id3;
151103
- }
151104
- return `${base2}-${randomBytes11(4).toString("hex")}`;
151105
- }
151106
- };
151107
-
151108
151603
  // packages/agent-host/src/dev-services/strays.ts
151109
- import { execFile as execFile12 } from "node:child_process";
151604
+ import { execFile as execFile13 } from "node:child_process";
151110
151605
  import { realpathSync as realpathSync11 } from "node:fs";
151111
151606
  import { promisify as promisify11 } from "node:util";
151112
151607
  import { userInfo as userInfo2 } from "node:os";
151113
- var exec10 = promisify11(execFile12);
151608
+ var exec10 = promisify11(execFile13);
151114
151609
  function parsePsRows(out) {
151115
151610
  const rows3 = [];
151116
151611
  for (const line of out.split("\n")) {
@@ -151382,7 +151877,7 @@ var toInfo = (h) => ({
151382
151877
  history: h.history(),
151383
151878
  alive: h.alive
151384
151879
  });
151385
- function createRpcHandler(mgr2, adapters2, { settleDeletedProjects, deleteOutlook, terminals: terminals2, updates: updates2, commands, externalApps: externalApps2, views: views2, inlineViews: inlineViews2, heldPids, themes: themes2, agentVersions: agentVersions2, sleeper: sleeper2, machines, stopHost, stopGate, handover: handover2, activity: activity2, updateApply: updateApply2, hubs: hubs2, mail, requests: requests2, serveCredentials } = {}) {
151880
+ function createRpcHandler(mgr2, adapters2, { settleDeletedProjects, deleteOutlook, terminals: terminals2, updates: updates2, commands, externalApps: externalApps2, views: views2, inlineViews: inlineViews2, heldPids, themes: themes2, agentVersions: agentVersions2, sleeper: sleeper2, machines, stopHost, stopGate, handover: handover2, systemEnding: systemEnding2, activity: activity2, updateApply: updateApply2, hubs: hubs2, mail, requests: requests2, serveCredentials } = {}) {
151386
151881
  const reportUiCrash = createUiCrashLog((line) => console.error(line));
151387
151882
  const requireThemes = () => {
151388
151883
  if (!themes2) throw rpcError("Theme files are unavailable");
@@ -151470,6 +151965,10 @@ function createRpcHandler(mgr2, adapters2, { settleDeletedProjects, deleteOutloo
151470
151965
  requireTerminals().close(RpcMethods["terminal.close"].params.parse(p).terminalId);
151471
151966
  return { ok: true };
151472
151967
  },
151968
+ "themes.remove": async (p) => {
151969
+ const keptAt = await requireThemes().remove(RpcMethods["themes.remove"].params.parse(p).id);
151970
+ return { removed: keptAt !== null, keptAt };
151971
+ },
151473
151972
  "themes.import": async (p) => requireThemes().importFrom(RpcMethods["themes.import"].params.parse(p).path),
151474
151973
  "themes.resolve": async (p) => ({ path: requireThemes().pathOf(RpcMethods["themes.resolve"].params.parse(p).id) }),
151475
151974
  "machines.add": async (p) => {
@@ -152072,7 +152571,8 @@ function createRpcHandler(mgr2, adapters2, { settleDeletedProjects, deleteOutloo
152072
152571
  if (updateApply2 && ctx) updateApply2.report(ctx.client, r);
152073
152572
  return {};
152074
152573
  },
152075
- "host.handover": async ({ handover: h }) => ack(handover2?.(h))
152574
+ "host.handover": async ({ handover: h }) => ack(handover2?.(h)),
152575
+ "host.systemEnding": async ({ cancelled: cancelled2 }) => ack(systemEnding2?.(cancelled2 === true))
152076
152576
  };
152077
152577
  const routes = { ...raw, ...handlers };
152078
152578
  return async (method, params, ctx) => {
@@ -152327,7 +152827,7 @@ function broadcastAppRuns(send, windowMs = APP_CHANGE_WINDOW_MS) {
152327
152827
  var sameCause = (a, b2) => a !== null && b2 !== null && JSON.stringify(a) === JSON.stringify(b2);
152328
152828
 
152329
152829
  // packages/agent-host/src/dev-services/terminal.ts
152330
- import { createRequire } from "node:module";
152830
+ import { createRequire as createRequire2 } from "node:module";
152331
152831
  import { existsSync as existsSync27 } from "node:fs";
152332
152832
 
152333
152833
  // packages/agent-host/src/dev-services/pty-slot.ts
@@ -152482,7 +152982,7 @@ function warnInternals(what) {
152482
152982
  }
152483
152983
 
152484
152984
  // packages/agent-host/src/dev-services/terminal.ts
152485
- var require2 = createRequire(import.meta.url);
152985
+ var require2 = createRequire2(import.meta.url);
152486
152986
  var TerminalService = class {
152487
152987
  constructor(emit, ptys) {
152488
152988
  this.emit = emit;
@@ -152675,8 +153175,8 @@ function shellPath() {
152675
153175
  }
152676
153176
 
152677
153177
  // packages/agent-host/src/dev-services/commands.ts
152678
- import { createRequire as createRequire2 } from "node:module";
152679
- var require3 = createRequire2(import.meta.url);
153178
+ import { createRequire as createRequire3 } from "node:module";
153179
+ var require3 = createRequire3(import.meta.url);
152680
153180
  var CommandRunner = class {
152681
153181
  constructor(emit, ptys) {
152682
153182
  this.emit = emit;
@@ -152824,7 +153324,7 @@ var CommandRunner = class {
152824
153324
  };
152825
153325
 
152826
153326
  // packages/agent-host/src/updates.ts
152827
- import { execFile as execFile13, execFileSync as execFileSync4 } from "node:child_process";
153327
+ import { execFile as execFile14, execFileSync as execFileSync4 } from "node:child_process";
152828
153328
  import { existsSync as existsSync28, readFileSync as readFileSync23 } from "node:fs";
152829
153329
  import { dirname as dirname25, join as join56 } from "node:path";
152830
153330
  var REGISTRY_URL = `https://registry.npmjs.org/${APP_SLUG}/latest`;
@@ -152847,7 +153347,7 @@ function hubUpdatesMachines(f) {
152847
153347
  async function npmGlobalPackage() {
152848
153348
  const cmd = commandFor("npm", ["root", "-g"]);
152849
153349
  const out = await new Promise((done2) => {
152850
- execFile13(cmd.file, cmd.args, { timeout: 6e4, windowsHide: true }, (err, stdout) => done2(err ? "" : String(stdout)));
153350
+ execFile14(cmd.file, cmd.args, { timeout: 6e4, windowsHide: true }, (err, stdout) => done2(err ? "" : String(stdout)));
152851
153351
  });
152852
153352
  const root3 = out.trim().split(/\r?\n/).at(-1)?.trim();
152853
153353
  if (!root3) return null;
@@ -152922,7 +153422,7 @@ function failureLine(e) {
152922
153422
  function runCommand(file2, args, resolve20) {
152923
153423
  const cmd = commandFor(file2, args, resolve20);
152924
153424
  return new Promise((done2, reject) => {
152925
- execFile13(cmd.file, cmd.args, { timeout: INSTALL_TIMEOUT_MS, windowsHide: true }, (err, _stdout, stderr) => {
153425
+ execFile14(cmd.file, cmd.args, { timeout: INSTALL_TIMEOUT_MS, windowsHide: true }, (err, _stdout, stderr) => {
152926
153426
  if (!err) return done2();
152927
153427
  if (err.code === "ENOENT") {
152928
153428
  return reject(new Error(`${file2} was not found on PATH (${err.message})`));
@@ -154369,7 +154869,14 @@ var Shutdown = class {
154369
154869
  if (this.ending) return this.ending;
154370
154870
  if (this.stopping) return this.stopping.catch(() => {
154371
154871
  });
154372
- const stopped = this.stopServices(mode, handOver2);
154872
+ this.mode = mode;
154873
+ let before;
154874
+ try {
154875
+ before = this.o.beforeLeave?.()?.catch(() => {
154876
+ });
154877
+ } catch {
154878
+ }
154879
+ const stopped = before ? before.then(() => this.stopServices(mode, handOver2)) : this.stopServices(mode, handOver2);
154373
154880
  const setTimer = this.o.setTimer ?? ((f, ms3) => void setTimeout(f, ms3).unref());
154374
154881
  setTimer(() => this.force(`did not finish within ${this.o.boundMs ?? SHUTDOWN_BOUND_MS}ms`), this.o.boundMs ?? SHUTDOWN_BOUND_MS);
154375
154882
  this.ending = stopped.catch((err) => {
@@ -154496,6 +155003,29 @@ function markQuit(dataDir, now = Date.now()) {
154496
155003
  } catch {
154497
155004
  }
154498
155005
  }
155006
+ function writeSystemEnd(dataDir) {
155007
+ return (end) => {
155008
+ try {
155009
+ if (end) writeFileSync10(join57(dataDir, SYSTEM_END_FILE), systemEndText(end));
155010
+ else rmSync8(join57(dataDir, SYSTEM_END_FILE), { force: true });
155011
+ } catch {
155012
+ }
155013
+ };
155014
+ }
155015
+ function takeSystemEnd(dataDir) {
155016
+ const path2 = join57(dataDir, SYSTEM_END_FILE);
155017
+ let text3;
155018
+ try {
155019
+ text3 = readFileSync24(path2, "utf8");
155020
+ } catch {
155021
+ return null;
155022
+ }
155023
+ try {
155024
+ rmSync8(path2, { force: true });
155025
+ } catch {
155026
+ }
155027
+ return parseSystemEnd(text3);
155028
+ }
154499
155029
  function updateResumeFolder(dataDir) {
154500
155030
  const NAME3 = /^update-resume\.[0-9a-f-]{36}\.json$/;
154501
155031
  return {
@@ -157283,10 +157813,6 @@ var LinkedMachineConnection = class {
157283
157813
  continuesCutTurns() {
157284
157814
  return this.updateContinued !== null;
157285
157815
  }
157286
- /** When the remote host took up this session's turn (its own id) after an update cut it (#1618); null when it did not */
157287
- continuedAfterUpdate(sessionId) {
157288
- return this.updateContinued?.get(sessionId) ?? null;
157289
- }
157290
157816
  info() {
157291
157817
  return {
157292
157818
  id: this.record.id,
@@ -159278,10 +159804,6 @@ function linkedSessions(machines, route = notRouted, hubName2, options = {}) {
159278
159804
  continuesCutTurns(machineId) {
159279
159805
  return machineById(machineId)?.continuesCutTurns?.() === true;
159280
159806
  },
159281
- continuedAfterUpdate(sessionId) {
159282
- const at2 = qualifiedMachine(machines(), sessionId);
159283
- return at2?.machine.continuedAfterUpdate?.(at2.id) ?? null;
159284
- },
159285
159807
  async backgroundTasks(machineId) {
159286
159808
  const m = machineById(machineId);
159287
159809
  if (!m || !m.reachable) return null;
@@ -159712,6 +160234,7 @@ var ROUTES = {
159712
160234
  "themes.list": HUB,
159713
160235
  "themes.save": HUB,
159714
160236
  "themes.import": HUB,
160237
+ "themes.remove": HUB,
159715
160238
  "themes.resolve": HUB,
159716
160239
  "updates.status": HUB,
159717
160240
  "updates.setAuto": HUB,
@@ -159736,6 +160259,8 @@ var ROUTES = {
159736
160259
  "host.stop": HUB,
159737
160260
  // A sign-in's serve on this machine telling its own host about the hand-over (#1224); never through a link
159738
160261
  "host.handover": HUB,
160262
+ // The window of this machine telling its own host the system is ending (#1970); never through a link
160263
+ "host.systemEnding": HUB,
159739
160264
  "machines.acceptVersions": HUB,
159740
160265
  "machines.install": HUB,
159741
160266
  "machines.update": HUB,
@@ -160313,7 +160838,7 @@ function reverseCalls(o) {
160313
160838
  }
160314
160839
 
160315
160840
  // packages/agent-host/src/lifecycle/work-job.ts
160316
- import { execFile as execFile14 } from "node:child_process";
160841
+ import { execFile as execFile15 } from "node:child_process";
160317
160842
  import { promisify as promisify12 } from "node:util";
160318
160843
  var WORK_JOB_LIMITS = 8192;
160319
160844
  var JOIN_SCRIPT = (pid, limits) => `
@@ -160354,7 +160879,7 @@ public static class CentraluWorkJob {
160354
160879
  '@
160355
160880
  [CentraluWorkJob]::Join(${pid}, ${limits})
160356
160881
  `;
160357
- var run3 = promisify12(execFile14);
160882
+ var run3 = promisify12(execFile15);
160358
160883
  var REAL_OS2 = {
160359
160884
  needed: endsWorkWithJob(),
160360
160885
  powershell: async (script) => {
@@ -160387,7 +160912,7 @@ function workJobLine(job) {
160387
160912
  // packages/agent-host/src/dev-services/broker-pty.ts
160388
160913
  import { spawn as startProcess } from "node:child_process";
160389
160914
  import { existsSync as existsSync30 } from "node:fs";
160390
- import { createRequire as createRequire3 } from "node:module";
160915
+ import { createRequire as createRequire4 } from "node:module";
160391
160916
  import { createInterface as createInterface2 } from "node:readline";
160392
160917
  import { fileURLToPath as fileURLToPath7 } from "node:url";
160393
160918
  function parseBrokerEvent(line) {
@@ -160563,7 +161088,7 @@ function openInHost(url2) {
160563
161088
  } catch {
160564
161089
  }
160565
161090
  }
160566
- var require4 = createRequire3(import.meta.url);
161091
+ var require4 = createRequire4(import.meta.url);
160567
161092
  function loadNodePty() {
160568
161093
  const loaded = require4("node-pty");
160569
161094
  if (!isPtyModule(loaded)) throw new Error("node-pty did not load as a terminal module");
@@ -160589,7 +161114,7 @@ function brokerPtyModule(broker, fallback = loadNodePty) {
160589
161114
  }
160590
161115
 
160591
161116
  // packages/agent-host/src/elevation.ts
160592
- import { execFile as execFile15 } from "node:child_process";
161117
+ import { execFile as execFile16 } from "node:child_process";
160593
161118
  function detectToken(o = {}) {
160594
161119
  const source = elevationSource(o.platform ?? HOST_PLATFORM, o.env ?? process.env);
160595
161120
  if (source.kind === "uid") {
@@ -160597,7 +161122,7 @@ function detectToken(o = {}) {
160597
161122
  return Promise.resolve({ elevated: getuid ? getuid() === 0 : null, logon: null });
160598
161123
  }
160599
161124
  const run4 = o.run ?? ((file2, args) => new Promise(
160600
- (resolve20, reject) => execFile15(file2, args, { windowsHide: true, timeout: 1e4, encoding: "utf8" }, (err, stdout) => err ? reject(err) : resolve20(stdout))
161125
+ (resolve20, reject) => execFile16(file2, args, { windowsHide: true, timeout: 1e4, encoding: "utf8" }, (err, stdout) => err ? reject(err) : resolve20(stdout))
160601
161126
  ));
160602
161127
  return run4(source.file, source.args).then(
160603
161128
  (out) => ({ elevated: elevatedFromGroups(out), logon: logonFromGroups(out) }),
@@ -160622,7 +161147,7 @@ var handedToken = launch.tokenOnStdin ? await readHandedToken(process.stdin).cat
160622
161147
  console.error(`[agent-host] the launcher did not hand over the token: ${e.message}`);
160623
161148
  process.exit(1);
160624
161149
  }) : launch.token;
160625
- var BUILD = true ? "7a978ec" : "dev";
161150
+ var BUILD = true ? "eafd282" : "dev";
160626
161151
  var { values } = parseArgs({
160627
161152
  options: {
160628
161153
  port: { type: "string", default: "5175" },
@@ -160694,6 +161219,30 @@ if (!lock.ok) {
160694
161219
  }
160695
161220
  var ownership = lock;
160696
161221
  process.on("exit", ownership.release);
161222
+ var systemEnd = dbPath === ":memory:" ? null : takeSystemEnd(dirname26(dbPath));
161223
+ if (systemEnd) console.error(`[agent-host] the last host was told the system was ending, at ${new Date(systemEnd.at).toISOString()}`);
161224
+ var systemEnding = new SystemEndNotice({
161225
+ write: dbPath === ":memory:" ? () => {
161226
+ } : writeSystemEnd(dirname26(dbPath)),
161227
+ outlived: () => console.error("[agent-host] still running well after the system said it was ending: it did not end")
161228
+ });
161229
+ var systemQuestion = null;
161230
+ var endSystemQuestion = () => {
161231
+ };
161232
+ process.on("exit", () => endSystemQuestion());
161233
+ var askSystem = () => {
161234
+ if (systemQuestion) return systemQuestion;
161235
+ const at2 = Date.now();
161236
+ const q2 = askSystemStopping();
161237
+ if (!q2.stopping) return void 0;
161238
+ endSystemQuestion = q2.end;
161239
+ systemQuestion = q2.stopping.then((stopping) => {
161240
+ if (!stopping) return;
161241
+ console.error("[agent-host] leaving as the system is stopping");
161242
+ systemEnding.notice(at2);
161243
+ });
161244
+ return systemQuestion;
161245
+ };
160697
161246
  if (dbPath !== ":memory:") clearStoppedMark(dirname26(dbPath));
160698
161247
  var updateResume = dbPath === ":memory:" ? void 0 : updateResumeFolder(dirname26(dbPath));
160699
161248
  start2.at("hold signals");
@@ -160730,7 +161279,12 @@ var mgr = new SessionManager(
160730
161279
  (sessionId) => bridgeAddress(launch.frontDoor, port, token, sessionId),
160731
161280
  // Worktrees are created next to the data folder — dev and the packaged app never touch each other's worktrees
160732
161281
  join59(dirname26(dbPath), "worktrees"),
160733
- { ...held2 ? { processes: held2.processes, keptSessions: held2.kept.sessionIds } : {}, ...updateResume ? { updateResume } : {} }
161282
+ {
161283
+ ...held2 ? { processes: held2.processes, keptSessions: held2.kept.sessionIds } : {},
161284
+ ...updateResume ? { updateResume } : {},
161285
+ // The last host recorded that the system was ending (#1397)
161286
+ ...systemEnd ? { systemEnd } : {}
161287
+ }
160734
161288
  );
160735
161289
  mgr.useProjectsHome(() => projectsHome());
160736
161290
  mgr.useMemoryGate(new MemoryGate());
@@ -160895,6 +161449,16 @@ var stopFromRequest = (o, by = "centralu serve --stop") => {
160895
161449
  else stopAsked = true;
160896
161450
  };
160897
161451
  if (control && launch.startedBy === "app") control.on("stop", (m) => stopFromRequest({ forUpdate: m.forUpdate === true }, "the window"));
161452
+ var systemEndingTold = (cancelled2) => {
161453
+ if (cancelled2) {
161454
+ console.error("[agent-host] the system is no longer ending");
161455
+ systemEnding.cancel();
161456
+ } else {
161457
+ console.error("[agent-host] the system is ending");
161458
+ systemEnding.notice();
161459
+ }
161460
+ };
161461
+ control?.on("system-ending", (m) => systemEndingTold(m.cancelled === true));
160898
161462
  var hubName = () => hostname5().replace(/\.local$/i, "") || "a linked computer";
160899
161463
  var links = new Links(linkDepsFor(store, {
160900
161464
  hub: { version: APP_VERSION, protocolVersion: PROTOCOL_VERSION, dev: BUILD === "dev" },
@@ -160980,6 +161544,7 @@ var router = new Router({
160980
161544
  // `host.stop {ifIdle}`: the check and the refusal of new calls in one step (#1224, lifecycle/stop-gate.ts)
160981
161545
  stopGate: launch.startedByServe && !launch.underKeeper ? createStopGate({ drain: hostDrain, activity: () => activityCounts(activity()) }) : void 0,
160982
161546
  // 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
161547
+ systemEnding: systemEndingTold,
160983
161548
  handover: (h) => {
160984
161549
  handover = h;
160985
161550
  server.broadcast({ type: "host_handover", handover: h });
@@ -161100,7 +161665,9 @@ var shutdown = new Shutdown({
161100
161665
  log: (line) => console.error(line),
161101
161666
  stopLog,
161102
161667
  exit: (code2) => process.exit(code2),
161103
- pid: process.pid
161668
+ pid: process.pid,
161669
+ // Whether the system is ending, asked as the leave begins, whichever way it began (#1397). Nothing where it cannot say (`undefined`)
161670
+ beforeLeave: () => askSystem()
161104
161671
  });
161105
161672
  start2.at("listen");
161106
161673
  await raceTimeout(elevation, 3e3, void 0);
@@ -161181,7 +161748,11 @@ if (control && launch.underKeeper) {
161181
161748
  return shutdown.stopServices(mode, true);
161182
161749
  },
161183
161750
  keepsAgents: KEEPS_AGENTS_ACROSS_SWAP && held2 !== null,
161184
- release: ownership.release,
161751
+ // The data folder is the next host's from here: nothing this one learns later is written in it
161752
+ release: () => {
161753
+ systemEnding.close();
161754
+ ownership.release();
161755
+ },
161185
161756
  write: (line) => void writeSync4(1, `${line}
161186
161757
  `),
161187
161758
  log: (line) => console.error(line),