@centralu/darwin-arm64 0.1.0-beta.37 → 0.1.0-beta.39

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.
@@ -43053,9 +43053,11 @@ import { hostname as hostname5 } from "node:os";
43053
43053
  import { dirname as dirname23, join as join52 } from "node:path";
43054
43054
  import { mkdirSync as mkdirSync19, writeSync as writeSync3 } from "node:fs";
43055
43055
 
43056
+ // packages/protocol/src/version.generated.ts
43057
+ var APP_VERSION = "0.1.0-beta.39";
43058
+
43056
43059
  // packages/protocol/src/brand.ts
43057
43060
  var APP_NAME = "Centralu";
43058
- var APP_VERSION = "0.1.0-beta.37";
43059
43061
  var APP_SLUG = "centralu";
43060
43062
  var CLIENT_INFO = { name: APP_SLUG, title: APP_NAME, version: APP_VERSION };
43061
43063
  var DATA_DIR = ".centralu";
@@ -59749,7 +59751,18 @@ var NormalizedEvent = external_exports.discriminatedUnion("type", [
59749
59751
  * The model the tool resolved the session's model name to (Claude's CLI: an alias such as `haiku` or the project's default
59750
59752
  * names a different model over time), when it says. With the window it is what `ModelWindows` keeps per model (#1376).
59751
59753
  */
59752
- model: external_exports.string().optional()
59754
+ model: external_exports.string().optional(),
59755
+ /**
59756
+ * (#1553) `window` is the model's full context window as the host says it, so the share is used / the model's window whatever
59757
+ * the tool reported (Claude's CLI reports the smaller of the model's window and the compaction point it was started with).
59758
+ * The host sets `window` and `capped`; `capped`: the full window is not known (an adopted process with no kept ceiling, a model
59759
+ * name that moved to another model), so `window` is only the tool's own reading, a floor under the model's.
59760
+ * `compactAt`: the size in tokens at which this session's process compacts by itself, said by the adapter of a process Centralu's
59761
+ * control started (Claude: the CLI's own threshold from every reading; Codex: the limit it was started with). The host drops one
59762
+ * that is not under `window`.
59763
+ */
59764
+ capped: external_exports.boolean().optional(),
59765
+ compactAt: external_exports.number().optional()
59753
59766
  }),
59754
59767
  external_exports.object({
59755
59768
  ...base,
@@ -60549,7 +60562,16 @@ var SessionInfo = external_exports.object({
60549
60562
  * tool and outlives us. So the store writes it down and reads it back (schema v17), and
60550
60563
  * `null` here means "this session has never reported one" rather than "we forgot".
60551
60564
  */
60552
- context: external_exports.object({ used: external_exports.number(), window: external_exports.number(), exactness: external_exports.enum(["exact", "estimate"]) }).nullable().default(null)
60565
+ context: external_exports.object({
60566
+ used: external_exports.number(),
60567
+ /** The model's full window when known (`capped` absent), else the tool's own reading of it (#1553, `contextShare`) */
60568
+ window: external_exports.number(),
60569
+ exactness: external_exports.enum(["exact", "estimate"]),
60570
+ /** `window` is a floor under the model's window, not the window itself */
60571
+ capped: external_exports.boolean().optional(),
60572
+ /** The size in tokens at which the session's process compacts by itself, when Centralu's control started it */
60573
+ compactAt: external_exports.number().optional()
60574
+ }).nullable().default(null)
60553
60575
  });
60554
60576
  var SettingsApplied = external_exports.enum(["restarted", "after_turn", "saved"]);
60555
60577
  var UpdateSettingsResult = SessionInfo.extend({ applied: SettingsApplied.optional() });
@@ -62458,6 +62480,17 @@ var sessionMethods = {
62458
62480
  params: external_exports.object({ sessionId: SessionId, seq: external_exports.number() }),
62459
62481
  result: external_exports.object({ ok: external_exports.literal(true) })
62460
62482
  },
62483
+ /**
62484
+ * Done on a session in `error` (#1549): it moves to `idle` and what the person saw is read, so the sidebar's mark and the
62485
+ * inbox row go. `seq` is the newest row the window had: the error is cleared only when nothing newer exists (a later error
62486
+ * the person has not seen stays), and rows up to `seq` are read either way. `cleared` says whether the state moved, `newer` that it stayed `error` because a newer row exists. A session
62487
+ * not in `error` keeps its state (its rows up to `seq` are read). Nothing is stopped, retried or continued, and the error
62488
+ * line stays in the conversation. The person's alone (the window): an agent's script may not hide a failure.
62489
+ */
62490
+ "sessions.clearError": {
62491
+ params: external_exports.object({ sessionId: SessionId, seq: external_exports.number() }),
62492
+ result: external_exports.object({ ok: external_exports.literal(true), cleared: external_exports.boolean(), newer: external_exports.boolean().default(false) })
62493
+ },
62461
62494
  "messages.load": {
62462
62495
  params: external_exports.object({ sessionId: SessionId, limit: external_exports.number().default(200), beforeSeq: external_exports.number().optional() }),
62463
62496
  result: external_exports.array(StoredMessage)
@@ -63463,12 +63496,12 @@ function applySessionState(m, e, now) {
63463
63496
  function nextContext(context, e) {
63464
63497
  switch (e.type) {
63465
63498
  case "context_update":
63466
- return { used: e.used, window: e.window, exactness: e.exactness };
63499
+ return { used: e.used, window: e.window, exactness: e.exactness, ...e.capped ? { capped: true } : {}, ...e.compactAt !== void 0 ? { compactAt: e.compactAt } : {} };
63467
63500
  case "conversation_reset":
63468
63501
  return null;
63469
63502
  case "compaction":
63470
63503
  if (e.failed || e.after === void 0 || !context) return context;
63471
- return { used: e.after, window: context.window, exactness: "estimate" };
63504
+ return { ...context, used: e.after, exactness: "estimate" };
63472
63505
  default:
63473
63506
  return context;
63474
63507
  }
@@ -65186,6 +65219,8 @@ var METHOD_RIGHTS = {
65186
65219
  "sessions.list": "everyday",
65187
65220
  "sessions.rename": "everyday",
65188
65221
  "sessions.markRead": "everyday",
65222
+ // Hiding a failure is the person's (an agent's script may not clear what went wrong with it)
65223
+ "sessions.clearError": "window",
65189
65224
  // The trash: reading and restoring run; deleting for good asks
65190
65225
  "trash.list": "everyday",
65191
65226
  "trash.readRun": "everyday",
@@ -103624,6 +103659,19 @@ var ModelWindows = class {
103624
103659
  if (known && known.window === window2 && known.model === (resolved ?? null)) return;
103625
103660
  this.write(key, { window: window2, model: resolved ?? null });
103626
103661
  }
103662
+ /**
103663
+ * The window a session's context share is read against (#1553): the model's full window, whatever the process reported. Asked
103664
+ * right after `note` took the same reading. A reading under the process's ceiling is the model's own window; one at it is a
103665
+ * floor, and the model's window is then what `note` kept (the record for this model), or unknown: the reading stands in as
103666
+ * `capped`, which the screen says. A tool with no ceiling (Codex, a process started without the setting) reports the model's own.
103667
+ */
103668
+ shareWindow(m, reported) {
103669
+ const ceiling = this.ceilings.get(m.id);
103670
+ const atCeiling = ceiling === "unknown" || ceiling !== void 0 && reported >= ceiling;
103671
+ const known = atCeiling ? this.of(m) : null;
103672
+ if (!atCeiling) return { window: reported, capped: false };
103673
+ return known === null ? { window: reported, capped: true } : { window: Math.max(known, reported), capped: false };
103674
+ }
103627
103675
  /** The model's window, or null when no session has read one */
103628
103676
  of(m) {
103629
103677
  return this.read(this.key(m))?.window ?? null;
@@ -104106,7 +104154,7 @@ var NOT_CONNECTED = "not connected until the person connects it";
104106
104154
  var NOT_CONNECTED_PHRASE = "is not connected, and nothing here connects it by itself; ask the person to connect it (Settings, Machines, Connect)";
104107
104155
  function awayPhrase(why) {
104108
104156
  if (why === NOT_CONNECTED) return NOT_CONNECTED_PHRASE;
104109
- return why === NOT_REACHABLE ? `is ${NOT_REACHABLE}` : `could not be asked just now (${why})`;
104157
+ return why === NOT_REACHABLE || why.startsWith(`${NOT_REACHABLE};`) ? `is ${why}` : `could not be asked just now (${why})`;
104110
104158
  }
104111
104159
 
104112
104160
  // packages/agent-host/src/adapters/contract/surface.ts
@@ -104115,6 +104163,168 @@ var SURFACE_KINDS = ["command", "message", "capability"];
104115
104163
  // packages/agent-host/src/adapters/contract.ts
104116
104164
  var INTERRUPTED_BY_TOOL = "interrupted_by_tool";
104117
104165
 
104166
+ // packages/agent-host/src/sessions/ask-project.ts
104167
+ import { realpathSync as realpathSync3, statSync as statSync8 } from "node:fs";
104168
+ import { resolve as resolve7 } from "node:path";
104169
+
104170
+ // packages/agent-host/src/sessions/turn-answer.ts
104171
+ function turnAnswer(messages2, opts = {}) {
104172
+ const parts = [];
104173
+ for (let i = messages2.length - 1; i >= 0; i--) {
104174
+ const m = messages2[i];
104175
+ if (opts.afterSeq !== void 0 && m.seq <= opts.afterSeq) break;
104176
+ if (m.role === "assistant" && m.kind === "reasoning") continue;
104177
+ if (m.role === "assistant" && m.kind === "text") {
104178
+ parts.unshift(String(m.payload.text ?? ""));
104179
+ continue;
104180
+ }
104181
+ break;
104182
+ }
104183
+ return parts.join("\n\n").trim();
104184
+ }
104185
+ function turnAnswerSeq(messages2, opts = {}) {
104186
+ let first = null;
104187
+ for (let i = messages2.length - 1; i >= 0; i--) {
104188
+ const m = messages2[i];
104189
+ if (opts.afterSeq !== void 0 && m.seq <= opts.afterSeq) break;
104190
+ if (m.role === "assistant" && m.kind === "reasoning") continue;
104191
+ if (m.role === "assistant" && m.kind === "text") {
104192
+ first = m.seq;
104193
+ continue;
104194
+ }
104195
+ break;
104196
+ }
104197
+ return first;
104198
+ }
104199
+ function lastResponseSeq(messages2) {
104200
+ let end = messages2.length;
104201
+ while (end > 0 && !(messages2[end - 1].role === "assistant" && messages2[end - 1].kind === "text")) end--;
104202
+ return turnAnswerSeq(messages2.slice(0, end));
104203
+ }
104204
+ function lastResponse(messages2) {
104205
+ let end = messages2.length;
104206
+ while (end > 0 && !(messages2[end - 1].role === "assistant" && messages2[end - 1].kind === "text")) end--;
104207
+ return turnAnswer(messages2.slice(0, end));
104208
+ }
104209
+ function clipPreview(text2, max) {
104210
+ return text2.length > max ? `${text2.slice(0, max)}\u2026` : text2;
104211
+ }
104212
+
104213
+ // packages/agent-host/src/sessions/ask-project.ts
104214
+ var ASK_WAIT_MS = 24e4;
104215
+ var ANSWER_READ_MAX_CHARS = 1e5;
104216
+ var ANSWER_MAX_CHARS = 6e3;
104217
+ var ANSWER_HEAD_CHARS = 4500;
104218
+ var ANSWER_TAIL_CHARS = 1200;
104219
+ function taskLine(task, max = 160) {
104220
+ const flat = oneLineText(task);
104221
+ return flat.length > max ? `${flat.slice(0, max)}\u2026 (cut here; ${flat.length} characters in all)` : flat;
104222
+ }
104223
+ function askFrame(callerProject, task, asker) {
104224
+ const who = asker ? ` It is the session ${JSON.stringify(frameField(asker.name))}${asker.sessionId ? ` [${asker.sessionId}]` : ""}.` : "";
104225
+ return [
104226
+ `[A session in the project "${frameField(callerProject)}" asks this project, through Centralu's ask_project.${who} Your final message goes back to it as the answer: keep it short, and name every file it should read by its absolute path.]`,
104227
+ "",
104228
+ task
104229
+ ].join("\n");
104230
+ }
104231
+ function projectChoice(p) {
104232
+ return `${p.name} [${p.id}] on ${p.machine ?? "this computer"}`;
104233
+ }
104234
+ function matchProjects(asked, projects, o = {}) {
104235
+ const wanted = asked.trim();
104236
+ const firstPerMachine = (found) => {
104237
+ if (o.everyOne) return found;
104238
+ const seen = /* @__PURE__ */ new Set();
104239
+ return found.filter((p) => !seen.has(p.machineId) && !!seen.add(p.machineId));
104240
+ };
104241
+ const tiers = [
104242
+ () => projects.filter((p) => p.id === wanted),
104243
+ () => projects.filter((p) => p.name === wanted),
104244
+ () => {
104245
+ const slash = wanted.indexOf("/");
104246
+ if (slash <= 0) return [];
104247
+ const machine = wanted.slice(0, slash).trim().toLowerCase();
104248
+ const name = wanted.slice(slash + 1).trim();
104249
+ const there = projects.filter((p) => p.machineId !== null && (p.machineId === machine || p.machine?.toLowerCase() === machine));
104250
+ return there.some((p) => p.name === name) ? there.filter((p) => p.name === name) : there.filter((p) => p.name.toLowerCase() === name.toLowerCase());
104251
+ },
104252
+ () => projects.filter((p) => p.name.toLowerCase() === wanted.toLowerCase())
104253
+ ];
104254
+ for (const tier of tiers) {
104255
+ const found = firstPerMachine(tier());
104256
+ if (found.length) return found;
104257
+ }
104258
+ return [];
104259
+ }
104260
+ function clipAnswer(answer, sessionName, at2) {
104261
+ if (answer.length <= ANSWER_MAX_CHARS) return answer;
104262
+ const cut2 = answer.length - ANSWER_HEAD_CHARS - ANSWER_TAIL_CHARS;
104263
+ const where2 = at2 ? `read them with read_session(sessionId="${at2.sessionId}", around=${at2.seq}, offset=${ANSWER_HEAD_CHARS})` : `the whole answer is in the session "${sessionName}"`;
104264
+ return answer.slice(0, ANSWER_HEAD_CHARS) + `
104265
+ \u2026[${cut2} characters cut here; ${where2}]\u2026
104266
+ ` + answer.slice(-ANSWER_TAIL_CHARS);
104267
+ }
104268
+ function answerAt(reader, sessionId, answer) {
104269
+ const rows3 = reader.loadMessages(sessionId, 50);
104270
+ const seq = turnAnswerSeq(rows3);
104271
+ return seq !== null && answer && turnAnswer(rows3) === answer ? { sessionId, seq } : void 0;
104272
+ }
104273
+ function pathsIn(answer) {
104274
+ const found = [];
104275
+ const re2 = /(?:^|[\s"'`(<[])((?:\/|[A-Za-z]:\\)[^\s"'`)>\]]+)/g;
104276
+ for (const m of answer.matchAll(re2)) {
104277
+ const p = m[1].replace(/[.,;:!?]+$/, "");
104278
+ if (p === "/" || /^[A-Za-z]:\\?$/.test(p)) continue;
104279
+ if (!found.includes(p)) found.push(p);
104280
+ }
104281
+ return found;
104282
+ }
104283
+ function real(p) {
104284
+ try {
104285
+ return realpathSync3.native(p);
104286
+ } catch {
104287
+ return null;
104288
+ }
104289
+ }
104290
+ function readableGrants(paths, projectRoot) {
104291
+ const root3 = real(projectRoot);
104292
+ const granted = [];
104293
+ const outside = [];
104294
+ for (const p of paths) {
104295
+ const r = real(p);
104296
+ if (!root3 || !r || !isInside(root3, r)) {
104297
+ outside.push(p);
104298
+ continue;
104299
+ }
104300
+ let dir = false;
104301
+ try {
104302
+ dir = statSync8(r).isDirectory();
104303
+ } catch {
104304
+ outside.push(p);
104305
+ continue;
104306
+ }
104307
+ if (dir && r === root3) {
104308
+ outside.push(p);
104309
+ continue;
104310
+ }
104311
+ if (!granted.includes(r)) granted.push(r);
104312
+ }
104313
+ return { granted, outside };
104314
+ }
104315
+ function underGrant(path2, grants, cwd) {
104316
+ const r = canonicalPath(resolve7(cwd, path2));
104317
+ for (const g5 of grants) if (isInside(g5, r)) return true;
104318
+ return false;
104319
+ }
104320
+ function awayFor(wanted, away, known) {
104321
+ const slash = wanted.indexOf("/");
104322
+ const prefix = slash > 0 ? wanted.slice(0, slash).trim().toLowerCase() : null;
104323
+ const byId = [...known, ...away.map((a) => a.machineId)].some((id3) => wanted.startsWith(`${id3}.`));
104324
+ if (prefix === null && !byId) return [...away];
104325
+ return away.filter((a) => a.machineId === prefix || a.machine.toLowerCase() === prefix || wanted.startsWith(`${a.machineId}.`));
104326
+ }
104327
+
104118
104328
  // packages/agent-host/src/sessions/app-agents.ts
104119
104329
  var AgentRunWait = class {
104120
104330
  constructor(notify, sessionName, onUsage = () => {
@@ -104221,11 +104431,11 @@ var AgentRunWait = class {
104221
104431
 
104222
104432
  // packages/agent-host/src/sessions/worktree-sessions.ts
104223
104433
  import { dirname as dirname14, join as join27 } from "node:path";
104224
- import { lstatSync as lstatSync8, statSync as statSync8 } from "node:fs";
104434
+ import { lstatSync as lstatSync8, statSync as statSync9 } from "node:fs";
104225
104435
  import { exec as exec2 } from "node:child_process";
104226
104436
 
104227
104437
  // packages/agent-host/src/dev-services/git.ts
104228
- import { isAbsolute as isAbsolute3, relative as relative2, resolve as resolve7 } from "node:path";
104438
+ import { isAbsolute as isAbsolute3, relative as relative2, resolve as resolve8 } from "node:path";
104229
104439
  function git(cwd, args, trust = {}) {
104230
104440
  return runGit(cwd, args, { allowRepoPrograms: trust.trusted === true });
104231
104441
  }
@@ -104319,8 +104529,8 @@ function assertLexicalGitPath(cwd, path2) {
104319
104529
  if (!path2 || path2.includes("\0") || path2.startsWith(":")) {
104320
104530
  throw rpcError("Invalid git path");
104321
104531
  }
104322
- const root3 = resolve7(cwd);
104323
- const target = isAbsolute3(path2) ? resolve7(path2) : resolve7(root3, path2);
104532
+ const root3 = resolve8(cwd);
104533
+ const target = isAbsolute3(path2) ? resolve8(path2) : resolve8(root3, path2);
104324
104534
  const rel = relative2(root3, target);
104325
104535
  if (rel === "") throw rpcError("Path is the project root, not a file in it");
104326
104536
  if (isInside(root3, target)) return rel;
@@ -104830,7 +105040,7 @@ async function prepareWorktreeDependencies(projectCwd, worktreeCwd) {
104830
105040
  // packages/agent-host/src/adapters/edit-grant.ts
104831
105041
  import { lstat as lstat7, realpath as realpath5 } from "node:fs/promises";
104832
105042
  import { homedir as homedir4 } from "node:os";
104833
- import { dirname as dirname13, isAbsolute as isAbsolute4, join as join26, relative as relative3, resolve as resolve8, sep as sep4 } from "node:path";
105043
+ import { dirname as dirname13, isAbsolute as isAbsolute4, join as join26, relative as relative3, resolve as resolve9, sep as sep4 } from "node:path";
104834
105044
  var PROTECTED_NAMES = /* @__PURE__ */ new Set([
104835
105045
  ".git",
104836
105046
  ".gitconfig",
@@ -104866,7 +105076,7 @@ var PROTECTED_NAMES = /* @__PURE__ */ new Set([
104866
105076
  var PROTECTED_PAIRS = [[".github", "workflows"], [".cargo", "config"], [".cargo", "config.toml"]];
104867
105077
  async function realPathOf(path2) {
104868
105078
  const tail = [];
104869
- let at2 = resolve8(path2);
105079
+ let at2 = resolve9(path2);
104870
105080
  for (; ; ) {
104871
105081
  try {
104872
105082
  return join26(await realpath5(at2), ...tail.reverse());
@@ -104893,7 +105103,7 @@ async function insideFolder(folder, targets) {
104893
105103
  }
104894
105104
  for (const target of targets) {
104895
105105
  if (!target) return false;
104896
- const real3 = await realPathOf(resolve8(folder, target));
105106
+ const real3 = await realPathOf(resolve9(folder, target));
104897
105107
  if (real3 === null) return false;
104898
105108
  const inside2 = relative3(root3, real3);
104899
105109
  if (inside2 === "" || isAbsolute4(inside2) || inside2 === ".." || inside2.startsWith(`..${sep4}`)) return false;
@@ -105132,7 +105342,7 @@ function worktreeFolderState(path2) {
105132
105342
  }
105133
105343
  const parent = dirname14(path2);
105134
105344
  try {
105135
- if (statSync8(parent).isDirectory()) return "gone";
105345
+ if (statSync9(parent).isDirectory()) return "gone";
105136
105346
  return { unreachable: `${parent} is not a folder` };
105137
105347
  } catch (e) {
105138
105348
  return { unreachable: `${parent} cannot be read (${e.code ?? "error"}), so the folder may be on a volume that is not mounted` };
@@ -105600,49 +105810,6 @@ function scriptLabel(raw) {
105600
105810
  return plain3 || "unnamed";
105601
105811
  }
105602
105812
 
105603
- // packages/agent-host/src/sessions/turn-answer.ts
105604
- function turnAnswer(messages2, opts = {}) {
105605
- const parts = [];
105606
- for (let i = messages2.length - 1; i >= 0; i--) {
105607
- const m = messages2[i];
105608
- if (opts.afterSeq !== void 0 && m.seq <= opts.afterSeq) break;
105609
- if (m.role === "assistant" && m.kind === "reasoning") continue;
105610
- if (m.role === "assistant" && m.kind === "text") {
105611
- parts.unshift(String(m.payload.text ?? ""));
105612
- continue;
105613
- }
105614
- break;
105615
- }
105616
- return parts.join("\n\n").trim();
105617
- }
105618
- function turnAnswerSeq(messages2, opts = {}) {
105619
- let first = null;
105620
- for (let i = messages2.length - 1; i >= 0; i--) {
105621
- const m = messages2[i];
105622
- if (opts.afterSeq !== void 0 && m.seq <= opts.afterSeq) break;
105623
- if (m.role === "assistant" && m.kind === "reasoning") continue;
105624
- if (m.role === "assistant" && m.kind === "text") {
105625
- first = m.seq;
105626
- continue;
105627
- }
105628
- break;
105629
- }
105630
- return first;
105631
- }
105632
- function lastResponseSeq(messages2) {
105633
- let end = messages2.length;
105634
- while (end > 0 && !(messages2[end - 1].role === "assistant" && messages2[end - 1].kind === "text")) end--;
105635
- return turnAnswerSeq(messages2.slice(0, end));
105636
- }
105637
- function lastResponse(messages2) {
105638
- let end = messages2.length;
105639
- while (end > 0 && !(messages2[end - 1].role === "assistant" && messages2[end - 1].kind === "text")) end--;
105640
- return turnAnswer(messages2.slice(0, end));
105641
- }
105642
- function clipPreview(text2, max) {
105643
- return text2.length > max ? `${text2.slice(0, max)}\u2026` : text2;
105644
- }
105645
-
105646
105813
  // packages/agent-host/src/sessions/transcript-view.ts
105647
105814
  var MEMORY_MESSAGES = 40;
105648
105815
  var MEMORY_LINE_CHARS = 600;
@@ -106011,10 +106178,29 @@ function orchestratorToolsFor(host, orchestratorId, scopeIds, ownProject, hubVie
106011
106178
  },
106012
106179
  createSession: async (opts) => {
106013
106180
  const all = host.store.projects.listProjects();
106014
- const local = all.filter((p) => p.id === opts.project || p.name === opts.project);
106015
106181
  const reader = linked();
106016
- const exactHere = all.some((p) => p.id === opts.project);
106017
- const remote = reader && opts.project && !exactHere ? await reader.findProjects(opts.project) : [];
106182
+ const wanted = (opts.project ?? "").trim();
106183
+ const exactHere = all.some((p) => p.id === wanted);
106184
+ const found = (machineProjects) => matchProjects(
106185
+ wanted,
106186
+ [
106187
+ ...all.map((p) => ({ id: p.id, name: p.name, machine: null, machineId: null })),
106188
+ ...machineProjects.map((p) => ({ id: p.id, name: p.name, machine: p.machine, machineId: p.machineId }))
106189
+ ],
106190
+ { everyOne: true }
106191
+ );
106192
+ let there = reader && wanted && !exactHere ? await reader.projects({ ref: wanted }) : null;
106193
+ let matched = found(there?.projects ?? []);
106194
+ if (reader && there && !matched.length) {
106195
+ there = await reader.projects({ ref: wanted, anyDown: true, prior: there });
106196
+ matched = found(there.projects);
106197
+ }
106198
+ const local = matched.flatMap((m) => all.filter((p) => m.machineId === null && p.id === m.id));
106199
+ const remote = matched.flatMap((m) => (there?.projects ?? []).filter((p) => m.machineId !== null && p.id === m.id));
106200
+ const missing = there && !matched.length ? awayFor(wanted, there.away, there.machines.map((m) => m.machineId)) : [];
106201
+ if (missing.length) {
106202
+ return { ok: false, error: `No such project: ${wanted}. Not asked, and its projects may be missing: ${missing.map((a) => `${a.machine} (${a.why})`).join(", ")}.` };
106203
+ }
106018
106204
  if (local.length + remote.length > 1) {
106019
106205
  const named = [
106020
106206
  ...local.map((p) => `${p.name} [${p.id}] on this computer`),
@@ -106494,8 +106680,8 @@ function askedQuestion(asker, act) {
106494
106680
  }
106495
106681
 
106496
106682
  // packages/agent-host/src/sessions/asked-reach.ts
106497
- import { realpathSync as realpathSync3 } from "node:fs";
106498
- import { dirname as dirname15, isAbsolute as isAbsolute5, join as join28, relative as relative4, resolve as resolve9 } from "node:path";
106683
+ import { realpathSync as realpathSync4 } from "node:fs";
106684
+ import { dirname as dirname15, isAbsolute as isAbsolute5, join as join28, relative as relative4, resolve as resolve10 } from "node:path";
106499
106685
  var GIVES_NOTHING = /* @__PURE__ */ new Set([
106500
106686
  "Write",
106501
106687
  "Edit",
@@ -106556,15 +106742,15 @@ function globPaths(path2, pattern) {
106556
106742
  return out.length > 0 ? out : [""];
106557
106743
  }
106558
106744
  function pinned(scope) {
106559
- return "paths" in scope ? { paths: scope.paths, real: scope.paths.map((p) => isAbsolute5(p) ? real(p) : p) } : scope;
106745
+ return "paths" in scope ? { paths: scope.paths, real: scope.paths.map((p) => isAbsolute5(p) ? real2(p) : p) } : scope;
106560
106746
  }
106561
- function real(path2) {
106562
- const full = resolve9(path2);
106747
+ function real2(path2) {
106748
+ const full = resolve10(path2);
106563
106749
  let existing = full;
106564
106750
  const rest = [];
106565
106751
  for (let i = 0; i < 64; i++) {
106566
106752
  try {
106567
- return join28(realpathSync3.native(existing), ...rest);
106753
+ return join28(realpathSync4.native(existing), ...rest);
106568
106754
  } catch {
106569
106755
  const up2 = dirname15(existing);
106570
106756
  if (up2 === existing) return full;
@@ -106593,10 +106779,10 @@ function outOfReach(name, scope, reach, projectOf) {
106593
106779
  const mine = [reach.project, reach.projectName].filter((p) => typeof p === "string" && p !== "");
106594
106780
  return mine.some((p) => p.toLowerCase() === asked) ? null : named(scope.project ?? "");
106595
106781
  }
106596
- const folders = reach.folders.filter((f) => isAbsolute5(f)).map(real);
106782
+ const folders = reach.folders.filter((f) => isAbsolute5(f)).map(real2);
106597
106783
  for (const [i, path2] of scope.paths.entries()) {
106598
106784
  if (!isAbsolute5(path2)) return path2 === "" ? name : named(path2);
106599
- const there = scope.real?.[i] ?? real(path2);
106785
+ const there = scope.real?.[i] ?? real2(path2);
106600
106786
  if (!folders.some((f) => inside(f, there))) return named(path2);
106601
106787
  }
106602
106788
  return null;
@@ -106734,114 +106920,6 @@ function withHubMark(settings, info) {
106734
106920
  // packages/agent-host/src/sessions/queued-requests.ts
106735
106921
  import { randomUUID as randomUUID11 } from "node:crypto";
106736
106922
 
106737
- // packages/agent-host/src/sessions/ask-project.ts
106738
- import { realpathSync as realpathSync4, statSync as statSync9 } from "node:fs";
106739
- import { resolve as resolve10 } from "node:path";
106740
- var ASK_WAIT_MS = 24e4;
106741
- var ANSWER_READ_MAX_CHARS = 1e5;
106742
- var ANSWER_MAX_CHARS = 6e3;
106743
- var ANSWER_HEAD_CHARS = 4500;
106744
- var ANSWER_TAIL_CHARS = 1200;
106745
- function taskLine(task, max = 160) {
106746
- const flat = oneLineText(task);
106747
- return flat.length > max ? `${flat.slice(0, max)}\u2026 (cut here; ${flat.length} characters in all)` : flat;
106748
- }
106749
- function askFrame(callerProject, task, asker) {
106750
- const who = asker ? ` It is the session ${JSON.stringify(frameField(asker.name))}${asker.sessionId ? ` [${asker.sessionId}]` : ""}.` : "";
106751
- return [
106752
- `[A session in the project "${frameField(callerProject)}" asks this project, through Centralu's ask_project.${who} Your final message goes back to it as the answer: keep it short, and name every file it should read by its absolute path.]`,
106753
- "",
106754
- task
106755
- ].join("\n");
106756
- }
106757
- function projectChoice(p) {
106758
- return `${p.name} [${p.id}] on ${p.machine ?? "this computer"}`;
106759
- }
106760
- function matchProjects(wanted, projects) {
106761
- const firstPerMachine = (found) => {
106762
- const seen = /* @__PURE__ */ new Set();
106763
- return found.filter((p) => !seen.has(p.machineId) && !!seen.add(p.machineId));
106764
- };
106765
- const tiers = [
106766
- () => projects.filter((p) => p.id === wanted),
106767
- () => projects.filter((p) => p.name === wanted),
106768
- () => {
106769
- const slash = wanted.indexOf("/");
106770
- if (slash <= 0) return [];
106771
- const machine = wanted.slice(0, slash).trim().toLowerCase();
106772
- const name = wanted.slice(slash + 1).trim();
106773
- const there = projects.filter((p) => p.machineId !== null && (p.machineId === machine || p.machine?.toLowerCase() === machine));
106774
- return there.some((p) => p.name === name) ? there.filter((p) => p.name === name) : there.filter((p) => p.name.toLowerCase() === name.toLowerCase());
106775
- },
106776
- () => projects.filter((p) => p.name.toLowerCase() === wanted.toLowerCase())
106777
- ];
106778
- for (const tier of tiers) {
106779
- const found = firstPerMachine(tier());
106780
- if (found.length) return found;
106781
- }
106782
- return [];
106783
- }
106784
- function clipAnswer(answer, sessionName, at2) {
106785
- if (answer.length <= ANSWER_MAX_CHARS) return answer;
106786
- const cut2 = answer.length - ANSWER_HEAD_CHARS - ANSWER_TAIL_CHARS;
106787
- const where2 = at2 ? `read them with read_session(sessionId="${at2.sessionId}", around=${at2.seq}, offset=${ANSWER_HEAD_CHARS})` : `the whole answer is in the session "${sessionName}"`;
106788
- return answer.slice(0, ANSWER_HEAD_CHARS) + `
106789
- \u2026[${cut2} characters cut here; ${where2}]\u2026
106790
- ` + answer.slice(-ANSWER_TAIL_CHARS);
106791
- }
106792
- function answerAt(reader, sessionId, answer) {
106793
- const rows3 = reader.loadMessages(sessionId, 50);
106794
- const seq = turnAnswerSeq(rows3);
106795
- return seq !== null && answer && turnAnswer(rows3) === answer ? { sessionId, seq } : void 0;
106796
- }
106797
- function pathsIn(answer) {
106798
- const found = [];
106799
- const re2 = /(?:^|[\s"'`(<[])((?:\/|[A-Za-z]:\\)[^\s"'`)>\]]+)/g;
106800
- for (const m of answer.matchAll(re2)) {
106801
- const p = m[1].replace(/[.,;:!?]+$/, "");
106802
- if (p === "/" || /^[A-Za-z]:\\?$/.test(p)) continue;
106803
- if (!found.includes(p)) found.push(p);
106804
- }
106805
- return found;
106806
- }
106807
- function real2(p) {
106808
- try {
106809
- return realpathSync4.native(p);
106810
- } catch {
106811
- return null;
106812
- }
106813
- }
106814
- function readableGrants(paths, projectRoot) {
106815
- const root3 = real2(projectRoot);
106816
- const granted = [];
106817
- const outside = [];
106818
- for (const p of paths) {
106819
- const r = real2(p);
106820
- if (!root3 || !r || !isInside(root3, r)) {
106821
- outside.push(p);
106822
- continue;
106823
- }
106824
- let dir = false;
106825
- try {
106826
- dir = statSync9(r).isDirectory();
106827
- } catch {
106828
- outside.push(p);
106829
- continue;
106830
- }
106831
- if (dir && r === root3) {
106832
- outside.push(p);
106833
- continue;
106834
- }
106835
- if (!granted.includes(r)) granted.push(r);
106836
- }
106837
- return { granted, outside };
106838
- }
106839
- function underGrant(path2, grants, cwd) {
106840
- const r = canonicalPath(resolve10(cwd, path2));
106841
- for (const g5 of grants) if (isInside(g5, r)) return true;
106842
- return false;
106843
- }
106844
-
106845
106923
  // packages/agent-host/src/sessions/interruptions.ts
106846
106924
  var NETWORK_WORDS = [
106847
106925
  /\b(ECONNREFUSED|ENOTFOUND|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|EHOSTUNREACH|ENETDOWN)\b/,
@@ -113241,11 +113319,12 @@ var RemoteAgents = class {
113241
113319
  if (!origin || !key) {
113242
113320
  return { error: `This project has ${origin ? "a local folder as its origin remote" : "no origin remote"}, so no checkout of it on ${machine.name} can be told apart. Start the agent here, without machine.` };
113243
113321
  }
113244
- const all = await reader.projects();
113322
+ const all = await reader.projects({ machineId: machine.id });
113245
113323
  const away = all.away.find((a) => a.machineId === machine.id);
113246
113324
  if (away) {
113247
113325
  if (away.why === NOT_CONNECTED) return { error: `${machine.name} ${awayPhrase(NOT_CONNECTED)}, so nothing was started there. Try again once it is connected.` };
113248
- return { error: `${machine.name} cannot be reached (${away.why}), so nothing was started there. Try again once its link is back.` };
113326
+ const cannot = away.why.startsWith(`${NOT_REACHABLE};`) ? `is ${away.why}` : `cannot be reached (${away.why})`;
113327
+ return { error: `${machine.name} ${cannot}, so nothing was started there. Try again once its link is back.` };
113249
113328
  }
113250
113329
  const theirs = all.projects.filter((p) => p.machineId === machine.id);
113251
113330
  const read = await Promise.all(theirs.map(async (p) => ({ p, r: await reader.origin(p) })));
@@ -119428,6 +119507,10 @@ var SessionStarts = class {
119428
119507
  autoCompactFor(m, adopted) {
119429
119508
  const { tokens } = autoCompactPlan(autoCompactOf(this.host.uiPreferences()), this.host.modelWindows, m);
119430
119509
  this.host.modelWindows.started(m.id, tokens === null ? null : this.host.adapters.get(m.tool)?.windowCeilingFor?.(tokens) ?? null, adopted);
119510
+ if (m.context?.compactAt !== void 0) {
119511
+ const { compactAt: _before, ...rest } = m.context;
119512
+ m.context = rest;
119513
+ }
119431
119514
  const kept = adopted ? this.host.modelWindows.ceilingOf(m.id) : null;
119432
119515
  return { ...tokens === null ? {} : { autoCompactTokens: tokens }, ...kept === null ? {} : { autoCompactCeiling: kept } };
119433
119516
  }
@@ -120320,7 +120403,15 @@ var ContextMeter = class {
120320
120403
  if (used === m.context.used || now - s.at < ESTIMATE_GAP_MS) return null;
120321
120404
  s.at = now;
120322
120405
  s.estimated = true;
120323
- return { type: "context_update", sessionId: m.id, used, window: m.context.window, exactness: "estimate" };
120406
+ return {
120407
+ type: "context_update",
120408
+ sessionId: m.id,
120409
+ used,
120410
+ window: m.context.window,
120411
+ exactness: "estimate",
120412
+ ...m.context.capped ? { capped: true } : {},
120413
+ ...m.context.compactAt !== void 0 ? { compactAt: m.context.compactAt } : {}
120414
+ };
120324
120415
  }
120325
120416
  };
120326
120417
 
@@ -120388,7 +120479,7 @@ var SessionEvents = class {
120388
120479
  meter = new ContextMeter();
120389
120480
  /** Receive event -> update metadata -> persist message -> broadcast */
120390
120481
  onEvent(raw, from = null) {
120391
- const e = raw.type === "files_touched" ? this.projectRelative(raw) : raw;
120482
+ let e = raw.type === "files_touched" ? this.projectRelative(raw) : raw;
120392
120483
  if (from && e.sessionId && this.host.handles.get(e.sessionId) !== from && e.type !== "approval_resolved" && e.type !== "question_resolved" && e.type !== "tool_result" && e.type !== "background_tasks") {
120393
120484
  return;
120394
120485
  }
@@ -120410,6 +120501,7 @@ var SessionEvents = class {
120410
120501
  this.host.compactEvent(e, false);
120411
120502
  return;
120412
120503
  }
120504
+ if (e.type === "context_update" && e.exactness !== "estimate") e = this.shareOf(e);
120413
120505
  if (e.type === "settings_changed" && e.by === "tool") {
120414
120506
  this.host.record.recordToolSettings(e);
120415
120507
  return;
@@ -120505,6 +120597,21 @@ var SessionEvents = class {
120505
120597
  if (e.type === "background_tasks" && e.sessionId) this.host.restartPolicy.applyDeferredRestart(e.sessionId);
120506
120598
  if (e.type === "background_tasks" && e.sessionId && this.host.children.pausedReports.has(e.sessionId)) this.host.reports.settlePausedReport(e.sessionId);
120507
120599
  }
120600
+ /**
120601
+ * The tool's reading of the context, as the rest of the host and the screens see it (#1553): `window` is the model's full window
120602
+ * (`ModelWindows.shareWindow`), `capped` says it is not known, `compactAt` is where the adapter says the session's process compacts. The reading
120603
+ * itself is what `ModelWindows` learns the model's window from, so it is noted first, against the window the tool reported. An
120604
+ * estimate from a request (#1492) is no reading of the model's window and carries what the last reading did (`ContextMeter`).
120605
+ */
120606
+ shareOf(e) {
120607
+ const m = this.host.meta.get(e.sessionId);
120608
+ if (!m) return e;
120609
+ this.host.modelWindows.note(m, e.window, e.model);
120610
+ const { window: window2, capped } = this.host.modelWindows.shareWindow(m, e.window);
120611
+ const { compactAt: said, ...rest } = e;
120612
+ const compactAt = said !== void 0 && said > 0 && said < window2 ? said : null;
120613
+ return { ...rest, window: window2, ...capped ? { capped: true } : {}, ...compactAt !== null ? { compactAt } : {} };
120614
+ }
120508
120615
  /** A state hint, for storage: the core reducer's rules on the host's copy (`applySessionState`, protocol session-state.ts) */
120509
120616
  applyStateHint(e, m) {
120510
120617
  const from = m.state;
@@ -120557,7 +120664,6 @@ var SessionEvents = class {
120557
120664
  break;
120558
120665
  case "context_update":
120559
120666
  m.context = nextContext(m.context, e);
120560
- if (e.exactness !== "estimate") this.host.modelWindows.note(m, e.window, e.model);
120561
120667
  break;
120562
120668
  case "conversation_reset":
120563
120669
  m.context = nextContext(m.context, e);
@@ -120995,7 +121101,8 @@ var SelfCompaction = class {
120995
121101
  if (now - since < IDLE_COMPACT_MINUTES * 6e4) return false;
120996
121102
  const ctx = m.context;
120997
121103
  if (!ctx || ctx.window <= 0) return false;
120998
- const window2 = this.host.window(m) ?? ctx.window;
121104
+ const window2 = this.host.window(m);
121105
+ if (window2 === null || window2 <= 0) return false;
120999
121106
  return ctx.used >= COMPACT_FLOOR_TOKENS && ctx.used >= window2 * setting.idleShare;
121000
121107
  }
121001
121108
  /** Starts the sweep. A host that never calls it still serves `compact_self` */
@@ -121060,7 +121167,7 @@ var ContextNudges = class {
121060
121167
  }
121061
121168
  cycle(id3) {
121062
121169
  let c = this.cycles.get(id3);
121063
- if (!c) this.cycles.set(id3, c = { told: /* @__PURE__ */ new Set(), waiting: null, size: 0, sending: false });
121170
+ if (!c) this.cycles.set(id3, c = { told: /* @__PURE__ */ new Set(), waiting: null, size: 0, sending: false, point: null });
121064
121171
  return c;
121065
121172
  }
121066
121173
  requested(id3, size) {
@@ -121068,25 +121175,31 @@ var ContextNudges = class {
121068
121175
  const setting = this.host.setting();
121069
121176
  const m = this.host.session(id3);
121070
121177
  if (!m || !setting.controlled || !setting.nudge || !this.host.hasCompactTool(id3)) return;
121071
- const window2 = this.host.window(m) ?? m.context?.window ?? 0;
121072
- if (window2 <= 0) return;
121178
+ const window2 = this.host.window(m);
121179
+ if (window2 === null || window2 <= 0) return;
121073
121180
  const c = this.cycle(id3);
121074
121181
  c.size = size;
121075
121182
  const share = size / window2;
121076
- const { first, second } = nudgeShares(setting);
121077
- const crossed = ["first", "second"].filter((k2) => !c.told.has(k2) && (k2 === "first" ? first : second) < setting.share && (k2 === "first" ? first : second) <= share);
121183
+ const point = m.context?.compactAt !== void 0 && m.context.compactAt < window2 ? m.context.compactAt / window2 : null;
121184
+ c.point = point;
121185
+ const { first, second } = nudgeShares({ ...setting, share: point ?? setting.share });
121186
+ const shares = [
121187
+ ["first", first],
121188
+ ["second", second]
121189
+ ];
121190
+ const crossed = shares.filter(([k2, at2]) => !c.told.has(k2) && (k2 === "first" ? at2 < (point ?? setting.share) : point !== null && at2 < point) && at2 <= share).map(([k2]) => k2);
121078
121191
  if (crossed.length > 0) {
121079
121192
  for (const k2 of crossed) c.told.add(k2);
121080
121193
  c.waiting = crossed.includes("second") ? "second" : "first";
121081
121194
  }
121082
- if (c.waiting) this.deliver(id3, c, m, window2, setting);
121195
+ if (c.waiting) this.deliver(id3, c, m, window2);
121083
121196
  }
121084
- deliver(id3, c, m, window2, setting) {
121197
+ deliver(id3, c, m, window2) {
121085
121198
  const which = c.waiting;
121086
121199
  if (!which || c.sending) return;
121087
121200
  const percent = Math.round(c.size / window2 * 100);
121088
121201
  const used = `${Math.round(c.size / 1e3)}k of ${Math.round(window2 / 1e3)}k tokens`;
121089
- const text2 = which === "second" ? `[Centralu] Your context is now about ${percent}% full (${used}), and the tool compacts it by itself in the middle of work at ${Math.round(setting.share * 100)}%. Call compact_self when the current step is done, at the latest; do not stop what you are doing for it.` : `[Centralu] Your context is now about ${percent}% full (${used}). When this step of the work is done, call compact_self so the next step starts from a summary; do not stop what you are doing for it.`;
121202
+ const text2 = which === "second" && c.point !== null ? `[Centralu] Your context is now about ${percent}% full (${used}), and the tool compacts it by itself in the middle of work at ${Math.round(c.point * 100)}%. Call compact_self when the current step is done, at the latest; do not stop what you are doing for it.` : `[Centralu] Your context is now about ${percent}% full (${used}). When this step of the work is done, call compact_self so the next step starts from a summary; do not stop what you are doing for it.`;
121090
121203
  const failed3 = (err) => {
121091
121204
  c.waiting = null;
121092
121205
  console.error(`[compact] ${id3.slice(0, 8)}: could not send the context notice: ${err instanceof Error ? err.message : String(err)}`);
@@ -121359,6 +121472,10 @@ function compactPointOff(passed, reported, modelWindow) {
121359
121472
  if (reported < expected && modelWindow !== null && modelWindow < passed) return null;
121360
121473
  return expected;
121361
121474
  }
121475
+ function claudeCompactAt(opts, threshold) {
121476
+ const controlled = opts.processSource?.adopt ? opts.autoCompactCeiling !== void 0 : opts.autoCompactTokens !== void 0;
121477
+ return controlled && threshold > 0 ? threshold : void 0;
121478
+ }
121362
121479
  function claudeAutoCompactWindow(tokens) {
121363
121480
  return Math.min(CLAUDE_AUTO_COMPACT_MAX, Math.max(CLAUDE_AUTO_COMPACT_MIN, Math.round(tokens) + CLAUDE_AUTO_COMPACT_BUFFER));
121364
121481
  }
@@ -125231,8 +125348,10 @@ var ClaudeSession = class {
125231
125348
  this.contextWindow = window2 > 0 ? window2 : null;
125232
125349
  if (window2 > 0 && used >= 0) {
125233
125350
  const model = typeof usage?.model === "string" && usage.model ? usage.model : void 0;
125234
- this.emit({ type: "context_update", sessionId: this.sessionId, used, window: window2, exactness: "exact", ...model ? { model } : {} });
125235
- this.checkCompactPoint(Number(usage?.autoCompactThreshold ?? 0));
125351
+ const threshold = Number(usage?.autoCompactThreshold ?? 0);
125352
+ const compactAt = claudeCompactAt(this.opts, threshold);
125353
+ this.emit({ type: "context_update", sessionId: this.sessionId, used, window: window2, exactness: "exact", ...model ? { model } : {}, ...compactAt !== void 0 ? { compactAt } : {} });
125354
+ this.checkCompactPoint(threshold);
125236
125355
  }
125237
125356
  } catch {
125238
125357
  }
@@ -127824,6 +127943,9 @@ async function initializeAppServer(client, sessionId, adopted, emit) {
127824
127943
  function autoCompactConfig(opts) {
127825
127944
  return opts.autoCompactTokens ? { model_auto_compact_token_limit: opts.autoCompactTokens } : {};
127826
127945
  }
127946
+ function codexCompactAt(opts) {
127947
+ return opts.autoCompactTokens && !opts.processSource?.adopt ? opts.autoCompactTokens : void 0;
127948
+ }
127827
127949
  function resumeThreadParams(opts, servers, workspaceConfig) {
127828
127950
  return {
127829
127951
  threadId: opts.resumeExternalId,
@@ -128360,7 +128482,9 @@ var CodexSession = class {
128360
128482
  /** A notification of this thread's own, as the events it maps to (`normalizeNotification`), each the way it goes out */
128361
128483
  emitNotification(n) {
128362
128484
  for (const said of normalizeNotification(this.sessionId, n)) {
128363
- const e = said.type === "compaction" ? { ...said, trigger: this.compactTurn ? "manual" : "auto" } : said;
128485
+ const sort = said.type === "compaction" ? { ...said, trigger: this.compactTurn ? "manual" : "auto" } : said;
128486
+ const compactAt = codexCompactAt(this.opts);
128487
+ const e = sort.type === "context_update" && compactAt !== void 0 ? { ...sort, compactAt } : sort;
128364
128488
  if (e.type === "error" && this.inputReceipts.size) this.interrupt(new Error(e.error.message));
128365
128489
  if (e.type === "message_image" && !e.data && e.path) {
128366
128490
  void imageEventFromDisk(this.sessionId, e.path).then((filled2) => this.emit(filled2));
@@ -135714,16 +135838,21 @@ var AskProjectFlow = class {
135714
135838
  const reader = this.host.linkedReader;
135715
135839
  const exactHere = projects.some((p) => p.id === wanted);
135716
135840
  const hubs2 = this.hubs;
135717
- const none = { projects: [], away: [] };
135718
- const [there, fromHubs] = (reader || hubs2) && !exactHere ? await Promise.all([reader ? reader.projects() : none, hubs2 ? hubs2.projects() : none]) : [none, none];
135719
- const askable = [
135841
+ const none = { projects: [], away: [], machines: [] };
135842
+ const [first, fromHubs] = (reader || hubs2) && !exactHere ? await Promise.all([reader ? reader.projects({ ref: wanted }) : none, hubs2 ? hubs2.projects() : none]) : [none, none];
135843
+ let there = first;
135844
+ const askableOf = () => [
135720
135845
  ...projects.map((p) => ({ id: p.id, name: p.name, machine: null, machineId: null })),
135721
135846
  ...there.projects.map((p) => ({ id: p.id, name: p.name, machine: p.machine, machineId: p.machineId })),
135722
135847
  ...fromHubs.projects.map((p) => ({ id: p.id, name: p.name, machine: p.hub, machineId: p.hubId }))
135723
135848
  ];
135849
+ let found = matchProjects(wanted, askableOf());
135850
+ if (!found.length && reader && !exactHere) {
135851
+ there = await reader.projects({ ref: wanted, anyDown: true, prior: there });
135852
+ found = matchProjects(wanted, askableOf());
135853
+ }
135724
135854
  const awayAll = [...there.away, ...fromHubs.away.map((a) => ({ machineId: a.hubId, machine: a.hub, why: a.why }))];
135725
135855
  const elsewhere = !!reader || fromHubs.projects.length > 0 || fromHubs.away.length > 0;
135726
- const found = matchProjects(wanted, askable);
135727
135856
  if (found.length > 1) {
135728
135857
  return {
135729
135858
  ok: false,
@@ -135740,7 +135869,8 @@ var AskProjectFlow = class {
135740
135869
  ...there.projects.map((p) => `${p.name} (on ${p.machine}${p.away ? `, ${p.away}` : ""})`),
135741
135870
  ...fromHubs.projects.map((p) => `${p.name} (on ${p.hub}${p.away ? `, ${p.away}` : ""})`)
135742
135871
  ];
135743
- const unasked = awayAll.length ? ` Not asked, and its projects may be missing: ${awayAll.map((a) => `${a.machine} (${a.why})`).join(", ")}.` : "";
135872
+ const missing = awayFor(wanted, awayAll, [...there.machines.map((m) => m.machineId), ...fromHubs.projects.map((p) => p.hubId)]);
135873
+ const unasked = missing.length ? ` Not asked, and its projects may be missing: ${missing.map((a) => `${a.machine} (${a.why})`).join(", ")}.` : "";
135744
135874
  return {
135745
135875
  ok: false,
135746
135876
  error: others.length ? `No project named "${wanted}". The other projects are: ${others.join(", ")}.${unasked}` : `No project named "${wanted}" \u2014 this is the only project in Centralu.${unasked}`
@@ -140450,6 +140580,21 @@ var SessionManager = class {
140450
140580
  rename(...a) {
140451
140581
  return this.sessionSettings.rename(...a);
140452
140582
  }
140583
+ /**
140584
+ * Done on a session in error (#1549): the person has read why, and nothing is left to do about the mark. It goes to `idle`
140585
+ * (the table allows error → idle) through the statement a resume makes, so every window hears it and a new error marks the
140586
+ * session again. The error line stays in the conversation; nothing is stopped or sent. Its last answer is read, so a stopped
140587
+ * turn that waits unread in the inbox goes too.
140588
+ */
140589
+ clearError(sessionId, seq) {
140590
+ const m = this.meta.get(sessionId);
140591
+ if (!m) throw sessionNotFound(sessionId);
140592
+ const newer = m.state === "error" && m.lastSeq > seq;
140593
+ const cleared = m.state === "error" && !newer;
140594
+ if (cleared) this.events.settleState(m, { type: "state_change", sessionId, state: "idle", reason: "error_cleared" });
140595
+ this.markRead(sessionId, seq);
140596
+ return { cleared, newer };
140597
+ }
140453
140598
  /** `SessionSettings.markRead` (session-settings.ts), kept on the manager for its callers */
140454
140599
  markRead(...a) {
140455
140600
  return this.sessionSettings.markRead(...a);
@@ -143766,6 +143911,10 @@ function createRpcHandler(mgr2, adapters2, { settleDeletedProjects, deleteOutloo
143766
143911
  mgr2.rename(sessionId, name);
143767
143912
  return { ok: true };
143768
143913
  },
143914
+ "sessions.clearError": async (p) => {
143915
+ const { sessionId, seq } = RpcMethods["sessions.clearError"].params.parse(p);
143916
+ return { ok: true, ...mgr2.clearError(sessionId, seq) };
143917
+ },
143769
143918
  "sessions.markRead": async (p) => {
143770
143919
  const { sessionId, seq } = RpcMethods["sessions.markRead"].params.parse(p);
143771
143920
  mgr2.markRead(sessionId, seq);
@@ -144805,6 +144954,13 @@ var AUTO_RETRY_DELAYS_MS = [10 * 60 * 1e3, 60 * 60 * 1e3, 6 * 60 * 60 * 1e3];
144805
144954
  var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
144806
144955
  var NPM_RETRY_DELAY_MS = 3e4;
144807
144956
  var NPM_RETRY_WINDOW_MS = 10 * 60 * 1e3;
144957
+ var UNSTAMPED_VERSION = "0.0.0-dev";
144958
+ function installsByItself(build3, current) {
144959
+ return build3 !== "dev" && current !== UNSTAMPED_VERSION;
144960
+ }
144961
+ function hubUpdatesMachines(f) {
144962
+ return installsByItself(f.build, f.current) && f.autoApply && !f.draining;
144963
+ }
144808
144964
  function existingInstalledCopy() {
144809
144965
  const path2 = installedCopyPath();
144810
144966
  return existsSync22(path2) ? path2 : null;
@@ -144855,6 +145011,8 @@ var UpdateService = class {
144855
145011
  constructor(publish, deps = {}) {
144856
145012
  this.publish = publish;
144857
145013
  this.deps = {
145014
+ current: deps.current ?? APP_VERSION,
145015
+ build: deps.build ?? "",
144858
145016
  fetchLatest: deps.fetchLatest ?? fetchLatestFromRegistry,
144859
145017
  run: deps.run ?? runCommand,
144860
145018
  installedCopy: deps.installedCopy ?? existingInstalledCopy,
@@ -144880,13 +145038,13 @@ var UpdateService = class {
144880
145038
  /*
144881
145039
  * The honest source for "which build is this".
144882
145040
  *
144883
- * `APP_VERSION` is what the app was compiled from, and `tooling/brand.test.ts`
144884
- * fails the build if it disagrees with the npm packages that carry it. The
144885
- * workspace root's package.json is not it — that one is private, has no version
144886
- * anyone installs, and would have read `0.0.0` forever without ever being wrong
144887
- * enough to notice.
145041
+ * `APP_VERSION` is what the app was compiled from: the version a release stamped
145042
+ * into the build with the npm packages that carry it (#1501, `tooling/brand.test.ts`),
145043
+ * or the placeholder `0.0.0-dev` of a source tree. The workspace root's package.json
145044
+ * is not it — that one is private, has no version anyone installs, and would have
145045
+ * read `0.0.0` forever without ever being wrong enough to notice.
144888
145046
  */
144889
- current: APP_VERSION,
145047
+ current: this.deps.current,
144890
145048
  latest: null,
144891
145049
  newer: false,
144892
145050
  auto: this.deps.readAuto(),
@@ -144983,6 +145141,7 @@ var UpdateService = class {
144983
145141
  */
144984
145142
  installIfAutomatic() {
144985
145143
  if (!this.status.autoApply || !this.status.newer || !this.status.latest) return;
145144
+ if (!installsByItself(this.deps.build, this.deps.current)) return;
144986
145145
  if (this.status.phase === "idle") {
144987
145146
  void this.apply();
144988
145147
  return;
@@ -145006,6 +145165,7 @@ var UpdateService = class {
145006
145165
  this.retryTimer = this.deps.schedule(() => {
145007
145166
  this.retryTimer = null;
145008
145167
  if (this.stopped || !this.status.autoApply || this.status.phase !== "failed" || !this.status.newer || !this.status.latest) return;
145168
+ if (!installsByItself(this.deps.build, this.deps.current)) return;
145009
145169
  void this.apply();
145010
145170
  }, delay);
145011
145171
  }
@@ -147380,8 +147540,8 @@ function failure2(target, code2, detail) {
147380
147540
  case "dev_dirty":
147381
147541
  return `The working copy on ${target} has ${detail} uncommitted change${detail === "1" ? "" : "s"}; commit or stash them, or pass --allow-dirty to build them too. Nothing was built`;
147382
147542
  case "dev_base":
147383
- if (/^0\.0\.0-dev(?:\.|$)/.test(detail))
147384
- return `The working copy on ${target} carries no release version (${detail}): its version is stamped from a release tag at build time (#1501), which this dev build cannot do. Nothing was built`;
147543
+ if (!detail || /^0\.0\.0-dev(?:\.|$)/.test(detail))
147544
+ return `The working copy on ${target} has no release tag (v*) reachable from its HEAD, which a dev build takes its version from (#1501); fetch the tags there (git fetch --tags) and try again. Nothing was built`;
147385
147545
  return `The working copy on ${target} is version ${detail || "(none)"}; a dev build is cut from a prerelease (x.y.z-beta.N)`;
147386
147546
  case "dev_protocol": {
147387
147547
  const [have = "?", want = "?"] = detail.split(" ");
@@ -148220,7 +148380,11 @@ var KEEPS_TRYING = "reconnecting";
148220
148380
  var WAITS_FOR_PERSON = "press Connect to try again";
148221
148381
  var NEXT_STEP = new RegExp(`; (${KEEPS_TRYING}|asking the machine again|${WAITS_FOR_PERSON})$`);
148222
148382
  var ATTEMPT_WAIT_MS = 6e4;
148383
+ var CALL_WAIT_MS = 1e4;
148384
+ var CALL_FAILURE_KEPT_MS = 6e4;
148385
+ var CALL_WINDOW_MS = 15e3;
148223
148386
  var AWAY = /* @__PURE__ */ new Set(["unreachable", "away", "not_running", "refused", "stopped"]);
148387
+ var CALL_RETRIES = /* @__PURE__ */ new Set(["unreachable", "away", "not_running", "refused"]);
148224
148388
  var LinkedMachineConnection = class {
148225
148389
  constructor(record2, tunnel, deps) {
148226
148390
  this.record = record2;
@@ -148335,6 +148499,10 @@ var LinkedMachineConnection = class {
148335
148499
  nextAttemptAt = null;
148336
148500
  /** Calls waiting for an attempt to connect to end (`settled`) */
148337
148501
  statusWaiters = /* @__PURE__ */ new Set();
148502
+ /** When a call last started an attempt (`reachForCall`), and how the attempt a call waited on ended, with when */
148503
+ lastCallAttemptAt = 0;
148504
+ callFailure = null;
148505
+ callFailureAt = 0;
148338
148506
  get id() {
148339
148507
  return this.record.id;
148340
148508
  }
@@ -148376,7 +148544,11 @@ var LinkedMachineConnection = class {
148376
148544
  }
148377
148545
  /** Resolves once no attempt is under way, or after `waitMs` (a minute): connecting took 2 to 4 s on a LAN, a host started there adds its start */
148378
148546
  settled(waitMs = this.deps.attemptWaitMs ?? ATTEMPT_WAIT_MS) {
148379
- if (!this.attemptUnderWay) return Promise.resolve();
148547
+ return this.waitUntil(() => !this.attemptUnderWay, waitMs);
148548
+ }
148549
+ /** Resolves once `ready()` holds (checked at every change of status and at the end of every opening), or after `waitMs` */
148550
+ waitUntil(ready, waitMs) {
148551
+ if (ready()) return Promise.resolve();
148380
148552
  return new Promise((resolve21) => {
148381
148553
  const done2 = () => {
148382
148554
  clearTimeout(timer);
@@ -148384,13 +148556,67 @@ var LinkedMachineConnection = class {
148384
148556
  resolve21();
148385
148557
  };
148386
148558
  const waiter = () => {
148387
- if (!this.attemptUnderWay) done2();
148559
+ if (ready()) done2();
148388
148560
  };
148389
148561
  const timer = setTimeout(done2, waitMs);
148390
148562
  timer.unref?.();
148391
148563
  this.statusWaiters.add(waiter);
148392
148564
  });
148393
148565
  }
148566
+ /**
148567
+ * An agent's call needs this machine (#1532): with Auto reconnect on and the link down, it does what the person's Connect
148568
+ * does (`connect`, the one action that opens a link now) instead of waiting out the backoff, and waits for it a short
148569
+ * bound (`CALL_WAIT_MS`). The caller reads `reachable` afterwards: connected in time, the call goes through; not, it is
148570
+ * answered as not reachable with `whyDown`, and the backoff goes on from where it was (`keepBackoff`).
148571
+ *
148572
+ * - one attempt at a time: a call that finds one under way (the backoff's, the person's press, another call's) waits on
148573
+ * it and starts none; so do ten calls at once, and a press at the same moment as a call;
148574
+ * - calls cannot defeat the backoff: at most one attempt started by a call per machine within `CALL_WINDOW_MS`; a call
148575
+ * inside the window waits for nothing and is answered at once;
148576
+ * - with Auto reconnect off nothing is started (#1341): the call is answered as not connected, and the person is named.
148577
+ * Neither does it when the person disconnected the machine, the host was stopped there, an operation holds the link
148578
+ * or the versions differ: a Connect does not help those, or is the person's to press
148579
+ */
148580
+ async reachForCall() {
148581
+ if (this.reachable) return;
148582
+ const until = Date.now() + (this.deps.callWaitMs ?? CALL_WAIT_MS);
148583
+ const left = () => Math.max(0, until - Date.now());
148584
+ await this.waitUntil(() => this.reachable || !this.redialing, left());
148585
+ if (this.reachable) return;
148586
+ if (this.attemptUnderWay) {
148587
+ await this.settled(Math.min(left(), this.deps.attemptWaitMs ?? ATTEMPT_WAIT_MS));
148588
+ return this.noteCallFailure();
148589
+ }
148590
+ if (this.stopped || this.parked || this.held || this.inOperation() || !this.autoReconnect || this.redialing) return;
148591
+ if (!CALL_RETRIES.has(this.status)) return;
148592
+ const now = Date.now();
148593
+ if (now - this.lastCallAttemptAt < (this.deps.callWindowMs ?? CALL_WINDOW_MS)) return;
148594
+ this.lastCallAttemptAt = now;
148595
+ this.connect({ start: false, keepBackoff: true });
148596
+ await this.settled(Math.min(left(), this.deps.attemptWaitMs ?? ATTEMPT_WAIT_MS));
148597
+ this.noteCallFailure();
148598
+ }
148599
+ /** The window in which a call starts at most one attempt (`reachForCall`) */
148600
+ get callWindowMs() {
148601
+ return this.deps.callWindowMs ?? CALL_WINDOW_MS;
148602
+ }
148603
+ /** How the attempt a call waited on ended when it did not connect: the machine's own words. One still going is told as it is (`whyDown`) */
148604
+ noteCallFailure() {
148605
+ if (this.reachable || this.attemptUnderWay) return;
148606
+ this.callFailure = this.error?.replace(NEXT_STEP, "") ?? null;
148607
+ this.callFailureAt = Date.now();
148608
+ }
148609
+ /**
148610
+ * What the answer to a call that could not take it says of the attempt to connect the machine (#1532), as a clause: that
148611
+ * one is still under way, or that the one a call made last failed, with the machine's own words. Null once the link is
148612
+ * up, and when no attempt was made or told of
148613
+ */
148614
+ get whyDown() {
148615
+ if (this.reachable) return null;
148616
+ if (this.attemptUnderWay) return "an attempt to connect it is still under way";
148617
+ if (!this.callFailure || Date.now() - this.callFailureAt > CALL_FAILURE_KEPT_MS) return null;
148618
+ return `the attempt to connect it just now failed: ${this.callFailure}`;
148619
+ }
148394
148620
  /** An install, update or other operation the person started is running: its retries are part of it (`retryLater`) */
148395
148621
  inOperation() {
148396
148622
  return this.held;
@@ -148526,7 +148752,9 @@ var LinkedMachineConnection = class {
148526
148752
  * The person's Connect (`machines.reconnect`, #1134): opens now, forgetting the backoff, also a disconnected link and
148527
148753
  * also with Auto reconnect off (#1341), where it is the only way to try again. It is also the person's Start
148528
148754
  * (#1143): a host is started there even where they stopped it or the link stopped starting them. `start: false` is
148529
- * the orchestrator's reconnect without its card, which opens the link and starts nothing the link would not
148755
+ * the orchestrator's reconnect without its card, which opens the link and starts nothing the link would not.
148756
+ * `keepBackoff` is an agent's call that needs the machine (`reachForCall`, #1532): it opens now too, but the backoff
148757
+ * goes on from the step it was at instead of starting over, so calls cannot hold it at its short steps
148530
148758
  */
148531
148759
  connect(opts = {}) {
148532
148760
  if (opts.start !== false) {
@@ -148535,7 +148763,7 @@ var LinkedMachineConnection = class {
148535
148763
  this.restartsSpent = false;
148536
148764
  }
148537
148765
  if (this.parked) return this.start();
148538
- this.reconnect();
148766
+ this.reconnect(opts.keepBackoff === true);
148539
148767
  }
148540
148768
  /**
148541
148769
  * Closes the link and tries none until the person connects it again (#1134). The machine stays linked; its sessions
@@ -148580,9 +148808,9 @@ var LinkedMachineConnection = class {
148580
148808
  this.start();
148581
148809
  }
148582
148810
  /** Opens again now, forgetting the backoff (`machines.reconnect`, or the person accepted the versions) */
148583
- reconnect() {
148811
+ reconnect(keepBackoff = false) {
148584
148812
  this.clearRetry();
148585
- this.attempt = 0;
148813
+ if (!keepBackoff) this.attempt = 0;
148586
148814
  const under = this.opening;
148587
148815
  if (under) {
148588
148816
  if (this.status === "away") this.setStatus("connecting", null);
@@ -148995,6 +149223,7 @@ var LinkedMachineConnection = class {
148995
149223
  setStatus(status, error51) {
148996
149224
  if (this.status === "connected" && status !== "connected") this.seen();
148997
149225
  const changed = status !== this.status || error51 !== this.error;
149226
+ if (status === "connected") this.callFailure = null;
148998
149227
  this.status = status;
148999
149228
  this.error = error51;
149000
149229
  if (changed) {
@@ -150301,9 +150530,12 @@ function linkedSessions(machines, route = notRouted, hubName2, options = {}) {
150301
150530
  for (const m of machines()) if (m.id === id3) return m;
150302
150531
  return null;
150303
150532
  };
150304
- const away = (m) => m.waitsForPerson ? `${m.name} ${awayPhrase(NOT_CONNECTED)}, then try again` : `${m.name} is ${NOT_REACHABLE}; try again once its link is back`;
150533
+ const away = (m) => m.waitsForPerson ? `${m.name} ${awayPhrase(NOT_CONNECTED)}, then try again` : `${m.name} is ${notReachable(m)}; try again once its link is back`;
150534
+ const notReachable = (m) => m.whyDown ? `${NOT_REACHABLE}; ${oneLine3(m.whyDown, 300)}` : NOT_REACHABLE;
150305
150535
  const settle = async (m) => {
150306
- if (m.attemptUnderWay) await m.settled?.();
150536
+ if (m.reachable) return;
150537
+ if (m.reachForCall) await m.reachForCall();
150538
+ else if (m.attemptUnderWay) await m.settled?.();
150307
150539
  };
150308
150540
  const attempt = async (m, run4) => {
150309
150541
  await settle(m);
@@ -150373,26 +150605,65 @@ function linkedSessions(machines, route = notRouted, hubName2, options = {}) {
150373
150605
  }
150374
150606
  return out;
150375
150607
  };
150376
- const everyProject = async () => {
150608
+ const projectsOf = async (m, tried = false) => {
150377
150609
  const away2 = [];
150378
- const perMachine = await Promise.all(
150379
- [...machines()].map(async (m) => {
150380
- const mirrored = (why) => {
150381
- away2.push({ machine: m.name, machineId: m.id, why });
150382
- return lastProjects(m).map((p) => ({ ...p, away: why }));
150383
- };
150384
- if (!m.reachable && m.attemptUnderWay) return lastProjects(m);
150385
- if (!m.reachable) return mirrored(m.waitsForPerson ? NOT_CONNECTED : NOT_REACHABLE);
150386
- try {
150387
- const list = await raceTimeout(m.call("projects.list", {}), lookupMs, TIMED_OUT);
150388
- if (list === TIMED_OUT) throw new Error(notWithin(lookupMs));
150389
- return (Array.isArray(list) ? list : []).map((raw) => projectOf(m, raw)).filter((p) => p !== null);
150390
- } catch (err) {
150391
- return mirrored(cause(m, err));
150392
- }
150610
+ const mirrored = (why) => {
150611
+ away2.push({ machine: m.name, machineId: m.id, why });
150612
+ return lastProjects(m).map((p) => ({ ...p, away: why }));
150613
+ };
150614
+ if (!m.reachable && m.attemptUnderWay && !tried) return { projects: lastProjects(m), away: away2 };
150615
+ if (!m.reachable) return { projects: mirrored(m.waitsForPerson ? NOT_CONNECTED : notReachable(m)), away: away2 };
150616
+ try {
150617
+ const list = await raceTimeout(m.call("projects.list", {}), lookupMs, TIMED_OUT);
150618
+ if (list === TIMED_OUT) throw new Error(notWithin(lookupMs));
150619
+ return { projects: (Array.isArray(list) ? list : []).map((raw) => projectOf(m, raw)).filter((p) => p !== null), away: away2 };
150620
+ } catch (err) {
150621
+ return { projects: mirrored(cause(m, err)), away: away2 };
150622
+ }
150623
+ };
150624
+ const qualifiedWith = (m, ref) => {
150625
+ const slash = ref.indexOf("/");
150626
+ const prefix = slash > 0 ? ref.slice(0, slash).trim().toLowerCase() : null;
150627
+ return prefix === m.id || prefix === m.name.toLowerCase() || ref.startsWith(`${m.id}.`);
150628
+ };
150629
+ const needs = (m, need3) => {
150630
+ if (need3.machineId === m.id) return true;
150631
+ const ref = need3.ref?.trim();
150632
+ if (!ref) return false;
150633
+ if (qualifiedWith(m, ref)) return true;
150634
+ return lastProjects(m).some((p) => p.id === ref || p.name.toLowerCase() === ref.toLowerCase());
150635
+ };
150636
+ const guessed = /* @__PURE__ */ new Map();
150637
+ const guess = async (m) => {
150638
+ const g5 = guessed.get(m.id);
150639
+ if (g5 && Date.now() - g5.at < (m.callWindowMs ?? 0)) return g5.wait ?? void 0;
150640
+ const entry = { at: Date.now(), wait: null };
150641
+ entry.wait = settle(m).finally(() => {
150642
+ entry.wait = null;
150643
+ });
150644
+ guessed.set(m.id, entry);
150645
+ return entry.wait;
150646
+ };
150647
+ const everyProject = async (need3) => {
150648
+ const all = [...machines()];
150649
+ const ref = need3?.ref?.trim();
150650
+ const bare2 = !!ref && !ref.includes("/") && !all.some((m) => qualifiedWith(m, ref));
150651
+ const anyDown = !!need3?.anyDown && bare2;
150652
+ const prior = need3?.prior;
150653
+ if (prior && !anyDown) return prior;
150654
+ const named = (m) => !!need3 && needs(m, need3);
150655
+ const wanted = all.filter((m) => !m.reachable && (anyDown || named(m)));
150656
+ const parts = await Promise.all(
150657
+ all.map(async (m) => {
150658
+ if (wanted.includes(m)) {
150659
+ await (anyDown && !named(m) ? guess(m) : settle(m));
150660
+ return projectsOf(m, true);
150661
+ }
150662
+ if (prior) return { projects: prior.projects.filter((p) => p.machineId === m.id), away: prior.away.filter((a) => a.machineId === m.id) };
150663
+ return projectsOf(m);
150393
150664
  })
150394
150665
  );
150395
- return { projects: perMachine.flat(), away: away2 };
150666
+ return { projects: parts.flatMap((p) => p.projects), away: parts.flatMap((p) => p.away), machines: all.map((m) => ({ machine: m.name, machineId: m.id })) };
150396
150667
  };
150397
150668
  return {
150398
150669
  async send(sessionId, text2, fromName, ask) {
@@ -150408,10 +150679,6 @@ function linkedSessions(machines, route = notRouted, hubName2, options = {}) {
150408
150679
  if (!m) return { ok: false, error: `Not a session of a linked machine: ${sessionId}` };
150409
150680
  return attempt(m, async () => ({ info: await route("agents.updateSettings", { sessionId, ...s, by: "orchestrator" }) }));
150410
150681
  },
150411
- async findProjects(ref) {
150412
- const wanted = ref.trim();
150413
- return (await everyProject()).projects.filter((p) => p.id === wanted || p.name === wanted);
150414
- },
150415
150682
  projects: everyProject,
150416
150683
  async projectSessions(project2) {
150417
150684
  const m = owner(project2.id);
@@ -150676,7 +150943,7 @@ function linkedSessions(machines, route = notRouted, hubName2, options = {}) {
150676
150943
  const m = owner(sessionId);
150677
150944
  if (!m) return { ok: false, error: `Not a session of a linked machine: ${sessionId}` };
150678
150945
  await settle(m);
150679
- if (!m.reachable) return { ok: false, error: m.waitsForPerson ? away(m) : `${m.name} is ${NOT_REACHABLE}` };
150946
+ if (!m.reachable) return { ok: false, error: m.waitsForPerson ? away(m) : `${m.name} is ${notReachable(m)}` };
150680
150947
  try {
150681
150948
  const answer = await m.call("host.lastAnswer", { sessionId: m.q.strip(sessionId, "The session"), maxChars: Math.min(maxChars, ANSWER_MAX_CHARS2) });
150682
150949
  const r5 = HostLastAnswerResult.safeParse(answer);
@@ -150716,7 +150983,7 @@ function linkedSessions(machines, route = notRouted, hubName2, options = {}) {
150716
150983
  const m = owner(sessionId);
150717
150984
  if (!m) return { ok: false, error: `Not a session of a linked machine: ${sessionId}` };
150718
150985
  await settle(m);
150719
- if (!m.reachable) return { ok: false, error: m.waitsForPerson ? away(m) : `${m.name} is not reachable right now; read it again once its link is back` };
150986
+ if (!m.reachable) return { ok: false, error: m.waitsForPerson ? away(m) : `${m.name} is ${notReachable(m)}; read it again once its link is back` };
150720
150987
  const toolMode = toolLinesOf(opts?.tools);
150721
150988
  try {
150722
150989
  const answer = await m.call("host.readSession", {
@@ -150883,6 +151150,7 @@ var ROUTES = {
150883
151150
  "attachments.save": session(),
150884
151151
  "sessions.rename": session(),
150885
151152
  "sessions.markRead": session(),
151153
+ "sessions.clearError": session(),
150886
151154
  "messages.load": session(messages),
150887
151155
  "messages.subagent": session(messages),
150888
151156
  "messages.subagentTrashed": session(),
@@ -151126,7 +151394,7 @@ var MachineUnreachableError = class extends Error {
151126
151394
  retryable = true;
151127
151395
  data;
151128
151396
  constructor(machine) {
151129
- super(`${machine.name} is not reachable right now; this works again once its link is back`);
151397
+ super(`${machine.name} is not reachable right now${machine.whyDown ? `; ${machine.whyDown}` : ""}; this works again once its link is back`);
151130
151398
  this.data = { machine: machine.id, reason: "unreachable" };
151131
151399
  }
151132
151400
  };
@@ -151191,7 +151459,8 @@ var Router = class {
151191
151459
  for (const m of this.opts.machines()) if (m.id === id3) return m;
151192
151460
  return null;
151193
151461
  }
151194
- async forward(m, method, params) {
151462
+ async forward(m, method, params, ctx) {
151463
+ if (!m.reachable && ctx === void 0) await m.reachForCall?.();
151195
151464
  if (!m.reachable) throw new MachineUnreachableError(m);
151196
151465
  return m.call(method, params);
151197
151466
  }
@@ -151201,7 +151470,7 @@ var Router = class {
151201
151470
  const m2 = this.machineById(params.machine);
151202
151471
  if (!m2) throw rpcError(`No linked machine ${params.machine}`);
151203
151472
  const { machine: _machine2, ...rest } = params;
151204
- const r5 = await this.forward(m2, method, route.params ? route.params(rest, m2.q) : rest);
151473
+ const r5 = await this.forward(m2, method, route.params ? route.params(rest, m2.q) : rest, ctx);
151205
151474
  return route.result ? route.result(r5, m2.q) : r5;
151206
151475
  }
151207
151476
  const m = this.owner(params[key]);
@@ -151212,7 +151481,7 @@ var Router = class {
151212
151481
  const done2 = this.opts.forwarding?.(method, params);
151213
151482
  let r;
151214
151483
  try {
151215
- r = await this.forward(m, method, p);
151484
+ r = await this.forward(m, method, p, ctx);
151216
151485
  } catch (e) {
151217
151486
  done2?.(false);
151218
151487
  throw e;
@@ -151247,7 +151516,7 @@ var Router = class {
151247
151516
  const { machine: _machine, ...own } = params;
151248
151517
  const p = route.orMachine ? own : { ...params };
151249
151518
  for (const key of route.keys) if (typeof p[key] === "string") p[key] = target.q.strip(p[key], "The id");
151250
- const r = await this.forward(target, method, p);
151519
+ const r = await this.forward(target, method, p, ctx);
151251
151520
  return route.result ? route.result(r, target.q) : r;
151252
151521
  }
151253
151522
  async byMachine(method, params, route, ctx) {
@@ -151259,7 +151528,7 @@ var Router = class {
151259
151528
  const shared = await this.sameAccountAnswer(m, method, rest, ctx);
151260
151529
  if (shared) return route.result ? route.result(shared, m.q) : shared;
151261
151530
  }
151262
- const r = await this.forward(m, method, rest);
151531
+ const r = await this.forward(m, method, rest, ctx);
151263
151532
  return route.result ? route.result(r, m.q) : r;
151264
151533
  }
151265
151534
  /**
@@ -151915,7 +152184,7 @@ var handedToken = launch.tokenOnStdin ? await readHandedToken(process.stdin).cat
151915
152184
  console.error(`[agent-host] the launcher did not hand over the token: ${e.message}`);
151916
152185
  process.exit(1);
151917
152186
  }) : launch.token;
151918
- var BUILD = true ? "49153ff" : "dev";
152187
+ var BUILD = true ? "bd398a1" : "dev";
151919
152188
  var { values } = parseArgs({
151920
152189
  options: {
151921
152190
  port: { type: "string", default: "5175" },
@@ -152178,6 +152447,8 @@ var { auto: autoUpdateCheck, autoApply: autoApplyUpdates } = updateSwitches(stor
152178
152447
  var themes = new ThemeFiles(join52(dataRoot(), "themes"), () => events.push({ type: "themes_changed" }));
152179
152448
  await themes.start().catch((e) => console.error(`[themes] the themes folder is unavailable: ${e.message}`));
152180
152449
  var updates = new UpdateService((status) => events.push({ type: "update_status", status }), {
152450
+ // A host from source never installs by itself, whatever is stored (#1501 review, `installsByItself`)
152451
+ build: BUILD,
152181
152452
  // On by default when nothing has been saved yet. Since the check is read-only and every failure
152182
152453
  // is swallowed, leaving it on costs nothing, while leaving it off traps **someone who never once
152183
152454
  // opens settings** on an old version forever — that person is exactly the one most likely to stay
@@ -152279,7 +152550,7 @@ var links = new Links({
152279
152550
  // When agents compact: this hub's setting governs the machines it links (#1376)
152280
152551
  autoCompact: () => autoCompactOf(mgr.uiPreferences()),
152281
152552
  // Automatic updates cover the linked machines too (#1508): one on an older release follows this hub's version; none starts mid-swap
152282
- autoUpdate: () => autoApplyUpdates.read() && !hostDrain.draining,
152553
+ autoUpdate: () => hubUpdatesMachines({ build: BUILD, current: APP_VERSION, autoApply: autoApplyUpdates.read(), draining: hostDrain.draining }),
152283
152554
  reverse: reverseCalls({
152284
152555
  allowed: (machineId) => store.machines.linkedMachineAsks(machineId),
152285
152556
  projects: () => mgr.projectsForMachines(),