@chibitek/chibi 0.1.155 → 0.1.158

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.
Files changed (98) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-B8LdsCr1.js → abnfDiagram-O67JEVCF-B8-8zzMS.js} +1 -1
  2. package/dist/assets/{arc-DU5fSmKU.js → arc-CSHQ9Dx3.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-DdQSIFKv.js → architectureDiagram-NJMV4G6O-BCHM9rXW.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-CaPcGYSS.js → blockDiagram-BEXU5L5S-B-5uxPFN.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-unt0ePgO.js → c4Diagram-YGBWAQC7-CuKNM_MR.js} +1 -1
  6. package/dist/assets/channel-bPN03jIt.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-Babg5Fzu.js → chunk-2Q5K7J3B-BNF02GM4.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-D1-dWVHo.js → chunk-3FUC2YCW-uEWgBd4M.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-DU7nSMbT.js → chunk-5VM5RSS4-eSZuXctY.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-QIjCJIhV.js → chunk-GTNCS2PH-CslS8nRS.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-pSByUE5R.js → chunk-GWA4HPMP-w71-MeKj.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-Dj2AaR1b.js → chunk-JWPE2WC7-Bjju7WGh.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-pZbN0-FP.js → chunk-NETBCI7D-HxrroD4M.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-Bqarwe4D.js → chunk-WEXAMYUT-D7Rz470x.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-WV8lm3aa.js → chunk-XXDRQBXY-BWKnAeyM.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-BANBtky1.js → classDiagram-v2-NBCMYWYE-CeBDRHmP.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-CtrktgBC.js → cose-bilkent-JH36ORCC-BBbRKlvr.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-DnhVVfba.js → cynefin-OW5HDTMX-W02cnJ0B.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-B42SRR6x.js → cynefinDiagram-VND7K2PF-Cs47eaxd.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-DM--vFMK.js → dagre-6A5THRUB-8mG18hzT.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-ZBupBrPU.js → diagram-22UHCM2B-DF4Qrw67.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-BPQzNIOd.js → diagram-3UASUU5V-3BAVoed1.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-5kO8vf8Z.js → diagram-ATOU4E4O-CKO3pECE.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-DtxogB86.js → diagram-CDSNMT55-CZhrGvur.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-Co9-nYw_.js → diagram-MLGK6HIB-C07l9pdQ.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-DWRuMh5y.js → diagram-MPIPVDR6-D3or4lkZ.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-DU_nmoG9.js → ebnfDiagram-ZINNZB2B-DV2epXp_.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-Bcje7S94.js → elk-276RUBZZ-DlvEn4-1.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-9aCo_2CI.js → erDiagram-OPXOYQCR-CB6X7Wt5.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-D4FYFyc5.js → flowDiagram-KWPJA3E3-C3bqz4lC.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-WQHX5rbx.js → ganttDiagram-FUAMR5RP-yo_Evhab.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-DRyzBdiV.js → gitGraphDiagram-X574FWY7-Cq2oUlap.js} +1 -1
  33. package/dist/assets/{index-CG1OvRgO.js → index-CG4AFPb4.js} +1 -1
  34. package/dist/assets/{index-D_8SD_0K.js → index-CMcr5k3Z.js} +205 -200
  35. package/dist/assets/index-CZjRcUdn.css +1 -0
  36. package/dist/assets/{infoDiagram-VRGFBTTK-D7SOmp4n.js → infoDiagram-VRGFBTTK-Y3ZrY7qL.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CdxOnDVX.js → ishikawaDiagram-OU5B5YK6-BpOtSfyi.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-CH7477Oa.js → journeyDiagram-ZHPQQLJL-UQM727le.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-BdpVvkHd.js → kanban-definition-PNTS6WVX-C3s5oHpY.js} +1 -1
  40. package/dist/assets/{linear-DTGq6qn5.js → linear-hX6nFOQr.js} +1 -1
  41. package/dist/assets/{mermaid.core-Sn2sv0Ti.js → mermaid.core-DZObGLN7.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-koCmkYuq.js → mindmap-definition-NLK3R4M7-Br-i2E3P.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-C3sqhm8e.js → pegDiagram-GJSIUBJH-D5SERB-N.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-BELBK122.js → pieDiagram-5QR66LMP-BPNf4J3D.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-Cksi5LSm.js → quadrantDiagram-O4NWA36T-Bm_Wh7Jr.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-f1av_EZW.js → railroadDiagram-XR7U4H2S-D6bo0abD.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-CW4O5nER.js → requirementDiagram-PLB6GJNP--1n5rkV7.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-Dz8ADHNY.js → sankeyDiagram-IPEJSGJF-Ct4tP2kK.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-Cc3HE0iZ.js → sequenceDiagram-PO4LG4MO-CCG7O5fR.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-gdaZNHnK.js → sizeCapture-INFHLROL-CJJJra9c.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK--u8LCnab.js → stateDiagram-v2-GCMORJYK-BMdRvVEX.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-4MAw4kdO.js → swimlanes-2SLR337P-B1wnogip.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-DWyjdGGR.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-CL89YkCT.js → timeline-definition-EJHVYXUP-l-3yhXUP.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-C92l7YQe.js → usecaseDiagram-POWQR4AR-DtwtCOoJ.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-17siYhSj.js → vennDiagram-UO4OBE2U-BZAEce1B.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-CNwa9VmN.js → wardleyDiagram-VNRHLVJA-Na_BCMMO.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-Dnu6Xzih.js → xychartDiagram-PMCCYNJV-BW9dlUfE.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/container-mcp.js +68 -14
  61. package/dist-server/index.js +5633 -4562
  62. package/dist-server/openmausbot.js +140 -81
  63. package/dist-server/pair-cli.js +140 -81
  64. package/dist-server/server/app-installers.js +107 -7
  65. package/dist-server/server/avatar-subscription.js +51 -0
  66. package/dist-server/server/cli-help.js +74 -0
  67. package/dist-server/server/cli.js +37 -78
  68. package/dist-server/server/colima-docker.js +136 -0
  69. package/dist-server/server/container-computer.js +31 -63
  70. package/dist-server/server/container-mcp.js +5 -0
  71. package/dist-server/server/device-routing.js +60 -0
  72. package/dist-server/server/drivers/claude.js +13 -11
  73. package/dist-server/server/drivers/local-inject.js +3 -4
  74. package/dist-server/server/engine-failover.js +22 -12
  75. package/dist-server/server/engine-tiers.js +5 -14
  76. package/dist-server/server/index.js +124 -120
  77. package/dist-server/server/personal-goal-runtime.js +86 -0
  78. package/dist-server/server/personal-goal-turn.js +125 -0
  79. package/dist-server/server/personal-goals.js +195 -0
  80. package/dist-server/server/port-in-use.js +21 -0
  81. package/dist-server/server/routes/local-models.js +3 -2
  82. package/dist-server/server/session-fallback.js +76 -0
  83. package/dist-server/server/skill-fetch.js +14 -0
  84. package/dist-server/server/vault/routes.js +43 -0
  85. package/dist-server/server/vault/runtime.js +48 -0
  86. package/dist-server/server/vault/store.js +177 -0
  87. package/dist-server/shared/desktop-private-port.js +1 -0
  88. package/dist-server/shared/engine-choices.js +15 -0
  89. package/dist-server/shared/engine-tiers.js +74 -2
  90. package/dist-server/shared/local-models.js +1 -3
  91. package/dist-server/shared/personal-goal.js +43 -0
  92. package/dist-server/shared/subscription-bots.js +3 -0
  93. package/dist-server/shared/vault.js +59 -0
  94. package/dist-server/vps-container-mcp.js +4 -2
  95. package/package.json +1 -1
  96. package/dist/assets/channel-GBaPOaz-.js +0 -1
  97. package/dist/assets/index-C8YP7Zxj.css +0 -1
  98. package/dist/assets/swimlanesDiagram-TC7HE7FX-CamSa0NE.js +0 -8
@@ -6,13 +6,13 @@
6
6
  // `cua-driver mcp` inside the container; this module never reimplements clicks,
7
7
  // typing, screenshots, accessibility, or window discovery.
8
8
  import { execFile } from "node:child_process";
9
- import { existsSync } from "node:fs";
10
9
  import { createHash, randomBytes } from "node:crypto";
11
10
  import { chmod, lstat, mkdir, mkdtemp, rename, rm, writeFile } from "node:fs/promises";
12
11
  import { tmpdir } from "node:os";
13
- import { dirname, join, resolve, win32 } from "node:path";
12
+ import { dirname, join, resolve } from "node:path";
14
13
  import { promisify } from "node:util";
15
14
  import { augmentedPath, resolveCliSpawn } from "./env-path.js";
15
+ import { colimaStartPlan, dockerEnvForColima, startColima } from "./colima-docker.js";
16
16
  import { spawnOptionsFor } from "./spawn-command.js";
17
17
  import { DATA_DIR } from "./config.js";
18
18
  import { SPAWNED_PROXIES } from "./proxy-paths.js";
@@ -188,7 +188,11 @@ async function sh(cmd, args, timeout = 8000) {
188
188
  timeout,
189
189
  encoding: "utf8",
190
190
  maxBuffer: 16 * 1024 * 1024,
191
- env: { ...process.env, PATH: augmentedPath() },
191
+ // Every docker call on a Mac goes to Colima's socket, whatever docker
192
+ // context Docker Desktop may have left active.
193
+ env: cmd === "docker"
194
+ ? dockerEnvForColima({ ...process.env, PATH: augmentedPath() })
195
+ : { ...process.env, PATH: augmentedPath() },
192
196
  }));
193
197
  return { stdout };
194
198
  }
@@ -233,77 +237,36 @@ export async function containerRuntimeStatus(runner = sh, platform = process.pla
233
237
  daemonUp: healthyIndex >= 0,
234
238
  };
235
239
  }
236
- const COLIMA_START_TIMEOUT_MS = 4 * 60_000;
237
- const DESKTOP_LAUNCH_TIMEOUT_MS = 30_000;
238
- export async function containerRuntimeStartPlan(runner = sh, platform = process.platform, appExists = existsSync, env = process.env) {
239
- if (platform === "darwin") {
240
- const context = await runner("docker", ["context", "show"], 4000).then(({ stdout }) => stdout.trim()).catch(() => "");
241
- const desktopApp = ["/Applications/Docker.app", join(env.HOME ?? "", "Applications", "Docker.app")].some((path) => appExists(path));
242
- const desktop = { label: "Docker Desktop", command: "open", args: ["-a", "Docker"], timeoutMs: DESKTOP_LAUNCH_TIMEOUT_MS };
243
- if (context === "desktop-linux" && desktopApp)
244
- return desktop;
245
- if (await installed("colima", runner, platform)) {
246
- // `colima start` on a running profile is a no-op, so an instance the
247
- // person started with their own flags is left exactly as it is.
248
- const profile = context.startsWith("colima-") ? context.slice("colima-".length) : "";
249
- return {
250
- label: "Colima",
251
- command: "colima",
252
- args: ["start", ...(profile ? ["--profile", profile] : [])],
253
- timeoutMs: COLIMA_START_TIMEOUT_MS,
254
- };
255
- }
256
- return desktopApp ? desktop : null;
257
- }
258
- if (platform === "win32") {
259
- const exe = win32.join(env.ProgramFiles ?? "C:\\Program Files", "Docker", "Docker", "Docker Desktop.exe");
260
- // Start-Process returns at once; Docker Desktop keeps running on its own.
261
- return appExists(exe)
262
- ? {
263
- label: "Docker Desktop",
264
- command: "powershell.exe",
265
- args: ["-NoProfile", "-NonInteractive", "-Command", `Start-Process -FilePath '${exe.replaceAll("'", "''")}'`],
266
- timeoutMs: DESKTOP_LAUNCH_TIMEOUT_MS,
267
- }
268
- : null;
269
- }
270
- return null;
240
+ /** How this machine's stopped Docker engine is started, or null when Atomic
241
+ * must not try: Colima on a Mac where it is installed, never Docker Desktop
242
+ * on any platform (colima-docker.ts). Linux needs root for the daemon and
243
+ * Windows runs the Local VM on Podman, so neither is started here. */
244
+ export async function containerRuntimeStartPlan(runner = sh, platform = process.platform, env = process.env) {
245
+ return platform === "darwin" && await installed("colima", runner, platform) ? colimaStartPlan(env) : null;
271
246
  }
272
247
  let runtimeStartInFlight = null;
273
- /** Start the person's installed Docker engine when it is stopped, then wait,
274
- * bounded, until `docker info` answers. Never installs anything and never
275
- * touches Podman or Apple container. One start at a time: concurrent callers
276
- * (startup and a turn) share the same attempt. Callers decide whether the
277
- * person allowed it; this only does it. */
248
+ let runtimeStartError = null;
249
+ /** Why the last start left the Local VM without an engine; null once docker answers. */
250
+ export const containerRuntimeError = () => runtimeStartError;
251
+ /** Start the person's installed Colima when docker is not answering, with a
252
+ * retry and a restart, bounded (startColima). Never installs anything, never
253
+ * opens Docker Desktop, never touches Podman or Apple container. One start at
254
+ * a time: concurrent callers (startup and a turn) share the same attempt.
255
+ * Callers decide whether the person allowed it; this only does it. */
278
256
  export function ensureContainerRuntime(opts = {}) {
279
257
  if (runtimeStartInFlight)
280
258
  return runtimeStartInFlight;
281
259
  const attempt = (async () => {
282
260
  const runner = opts.runner ?? sh;
283
261
  const platform = opts.platform ?? process.platform;
284
- const sleep = opts.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
285
262
  const before = await containerRuntimeStatus(runner, platform);
286
263
  if (before.daemonUp || before.runtime !== "docker")
287
- return { status: before, started: null };
288
- const plan = await containerRuntimeStartPlan(runner, platform, opts.appExists, opts.env);
289
- if (!plan)
290
- return { status: before, started: null };
291
- try {
292
- await runner(plan.command, plan.args, plan.timeoutMs);
293
- }
294
- catch {
295
- // Colima can exit non-zero while its VM is still coming up, and the
296
- // wait below is the real test; a launch that truly failed times out.
297
- }
298
- const deadline = Date.now() + (opts.waitMs ?? 120_000);
299
- let status = await containerRuntimeStatus(runner, platform);
300
- while (!status.daemonUp && Date.now() < deadline) {
301
- await sleep(opts.pollMs ?? 2000);
302
- status = await containerRuntimeStatus(runner, platform);
303
- }
304
- return { status, started: plan.label };
264
+ return { status: before, started: null, error: null };
265
+ const plan = await containerRuntimeStartPlan(runner, platform, opts.env);
266
+ return startColima(plan, before, { runner, platform, status: () => containerRuntimeStatus(runner, platform), ...opts });
305
267
  })();
306
268
  runtimeStartInFlight = attempt;
269
+ void attempt.then((outcome) => { runtimeStartError = outcome.status.daemonUp ? null : outcome.error; }, () => { });
307
270
  void attempt.finally(() => { if (runtimeStartInFlight === attempt)
308
271
  runtimeStartInFlight = null; }).catch(() => { });
309
272
  return attempt;
@@ -989,6 +952,11 @@ export function containerComputerMcp(runtime, control, target = SHARED_LOCAL_VM_
989
952
  // through `ps` for the life of the bridge.
990
953
  env: {
991
954
  ELECTRON_RUN_AS_NODE: "1",
955
+ // The bridge pins docker to Colima itself too (container-mcp.ts); this
956
+ // keeps a host that spawns it with a scrubbed environment on Colima.
957
+ ...(runtime === "docker" && process.platform === "darwin"
958
+ ? { DOCKER_HOST: dockerEnvForColima(process.env, "darwin").DOCKER_HOST ?? "" }
959
+ : {}),
992
960
  ...(control ? { OMB_CONTROL_URL: control.url, OMB_CONTROL_TOKEN: control.token } : {}),
993
961
  // Where the bridge remembers Cua Driver's tool list, and whether it
994
962
  // must stand in for Cua Driver until the first tool call (the VM is
@@ -1011,7 +979,7 @@ export function setupCommands(runtime, platform = process.platform, target = SHA
1011
979
  : runtime === "podman" && platform !== "linux"
1012
980
  ? "podman machine init; podman machine start"
1013
981
  : runtime === "docker" && platform === "darwin"
1014
- ? "colima start || open -a Docker"
982
+ ? "colima start"
1015
983
  : runtime === "docker" && platform === "linux"
1016
984
  ? "sudo systemctl start docker"
1017
985
  : null;
@@ -3,6 +3,8 @@
3
3
  // piping, drain-safe exit, and watchdog live in mcp-bridge.ts, shared with
4
4
  // the VPS entry point.
5
5
  import { cuaExecArgs } from "./container-computer.js";
6
+ import { dockerEnvForColima } from "./colima-docker.js";
7
+ import { augmentedPath } from "./env-path.js";
6
8
  import { runMcpBridge } from "./mcp-bridge.js";
7
9
  const [runtime, container, socket] = process.argv.slice(2);
8
10
  if (!runtime || !["docker", "podman", "container"].includes(runtime)) {
@@ -27,6 +29,9 @@ runMcpBridge({
27
29
  command: runtime,
28
30
  args: cuaExecArgs(["mcp", "--socket", socket], { container, interactive: true }),
29
31
  label: "Cua Driver",
32
+ // On a Mac, docker always talks to Colima's socket, never to whatever
33
+ // context Docker Desktop left active.
34
+ ...(runtime === "docker" ? { env: dockerEnvForColima({ ...process.env, PATH: augmentedPath() }) } : {}),
30
35
  // No liveness watchdog: the runtime CLI talks to a local daemon and fails
31
36
  // fast on its own — there is no silent WAN peer to wedge on.
32
37
  ...(controlUrl && controlToken ? { gate: { url: controlUrl, token: controlToken } } : {}),
@@ -0,0 +1,60 @@
1
+ // Device routing for approval requests (docs/specs/remote-approval/spec.md).
2
+ //
3
+ // The server tracks which user endpoint is live — the desktop's event
4
+ // stream or a paired phone's — and an approval request routes to the live
5
+ // user device. A remote session (no live desktop stream) routes to the
6
+ // phone automatically. It never broadcasts: the target is one device kind,
7
+ // picked here, and every client learns it from the stream's hello frame.
8
+ /** Where an approval goes. The desktop wins whenever it is live, so a
9
+ * person sitting at the Mac is never interrupted on the phone; a remote
10
+ * session routes to the phone; nothing live still answers "phone" with
11
+ * live: false, so the ask is queued for the one device that can travel to
12
+ * the person rather than broadcast. */
13
+ export function pickApprovalRoute(liveness) {
14
+ if (liveness.desktop > 0)
15
+ return { device: "desktop", live: true };
16
+ if (liveness.phone > 0)
17
+ return { device: "phone", live: true };
18
+ return { device: "phone", live: false };
19
+ }
20
+ /** The server's count of live user streams by device kind. Written by the
21
+ * SSE route's add/remove, read when an approval is raised or a client
22
+ * asks. Counts, never identities: routing is about which kind of device is
23
+ * present, not who is watching. */
24
+ export class DeviceRouteRegistry {
25
+ liveness = { desktop: 0, phone: 0 };
26
+ /** A stream opened. `kind` is the connection's auth kind: loopback is
27
+ * the desktop app on this machine; a paired session is a phone. */
28
+ streamOpened(kind) {
29
+ if (kind === "loopback")
30
+ this.liveness.desktop += 1;
31
+ else if (kind === "session")
32
+ this.liveness.phone += 1;
33
+ }
34
+ streamClosed(kind) {
35
+ if (kind === "loopback")
36
+ this.liveness.desktop = Math.max(0, this.liveness.desktop - 1);
37
+ else if (kind === "session")
38
+ this.liveness.phone = Math.max(0, this.liveness.phone - 1);
39
+ }
40
+ route() {
41
+ return pickApprovalRoute(this.liveness);
42
+ }
43
+ counts() {
44
+ return { ...this.liveness };
45
+ }
46
+ }
47
+ /** End one stream: drop it from the fan-out set, end the response, and
48
+ * tell approval routing the device went away. Every removal path goes
49
+ * through here so the routing counts cannot drift from the stream set. */
50
+ export function removeStream(clients, registry, client) {
51
+ if (!clients.delete(client))
52
+ return;
53
+ registry.streamClosed(client.authKind);
54
+ try {
55
+ client.res.end();
56
+ }
57
+ catch {
58
+ /* already gone */
59
+ }
60
+ }
@@ -1615,6 +1615,8 @@ export const ClaudeDriver = {
1615
1615
  // A settled turn owns no retry budget. Retained CLI sessions may run
1616
1616
  // many later turns on this thread, and each must start fresh.
1617
1617
  retryState.delete(threadId);
1618
+ if (t.authFailed)
1619
+ closeSession(threadId, "sign-in required");
1618
1620
  emit({ ...base(threadId, t.turnId), type: "turn.completed", ok, stopReason, cost, ...(usage ? { usage } : {}) });
1619
1621
  if (session.child.exitCode === null && !session.closing)
1620
1622
  armIdle(threadId);
@@ -1772,11 +1774,7 @@ export const ClaudeDriver = {
1772
1774
  case "assistant": {
1773
1775
  const msg = o.message ?? {};
1774
1776
  const text = firstText(msg.content);
1775
- // An unauthenticated turn comes back as an api-error frame whose
1776
- // only content is the CLI's own "run /login" instruction — a
1777
- // command this app has no terminal to run, so relaying it as a
1778
- // reply strands the user. Every other engine reports this as a
1779
- // setup error; that is what routes them to the sign-in card.
1777
+ // CLI auth diagnostics enter the shared fallback path, never assistant text.
1780
1778
  if (claudeAuthFailure(o, text)) {
1781
1779
  if (session.turn)
1782
1780
  session.turn.authFailed = true;
@@ -1853,12 +1851,16 @@ export const ClaudeDriver = {
1853
1851
  if (o.origin?.kind === "task-notification")
1854
1852
  break;
1855
1853
  {
1856
- // result.usage is this invocation's total — one process per turn,
1857
- // so it is the turn's figure. cache reads count as input: they
1858
- // are billed (at the cache rate) and they fill the window — but
1859
- // they are reported separately too, so the UI can show how much
1860
- // of the figure was context re-read rather than new text.
1854
+ // Result usage includes input, output and cached context.
1861
1855
  const t = session.turn;
1856
+ // Some CLI failures exist only on the result, with no assistant frame.
1857
+ const resultError = [o.result, ...(Array.isArray(o.errors) ? o.errors : [])]
1858
+ .filter((value) => typeof value === "string").join("\n");
1859
+ if (o.is_error === true && !t?.authFailed && claudeAuthFailure({ error: o.error ?? "api_error" }, resultError)) {
1860
+ if (t)
1861
+ t.authFailed = true;
1862
+ emit({ ...base(threadId, currentTurnId()), type: "runtime.error", message: resultError, setup: true, failure: "auth_failed" });
1863
+ }
1862
1864
  const usage = o.usage
1863
1865
  ? {
1864
1866
  input: (o.usage.input_tokens || 0) + (o.usage.cache_read_input_tokens || 0) + (o.usage.cache_creation_input_tokens || 0),
@@ -1870,7 +1872,7 @@ export const ClaudeDriver = {
1870
1872
  : undefined;
1871
1873
  const earlier = t?.carried;
1872
1874
  const result = {
1873
- ok: o.is_error !== true,
1875
+ ok: o.is_error !== true && !t?.authFailed,
1874
1876
  stopReason: t?.authFailed ? "auth_required"
1875
1877
  : t?.refreshConflict && o.is_error === true ? "credential_refresh_conflict"
1876
1878
  : claudeResultStopReason(o),
@@ -213,9 +213,8 @@ function idsFromModelsPayload(payload) {
213
213
  const low = id.toLowerCase();
214
214
  if (low.includes("embed") || low.includes("bge-") || low.includes("nomic"))
215
215
  return [];
216
- // An Ollama Cloud model is not this machine's: it bills a plan.
217
- if (isOllamaCloudModel(record))
218
- return [];
216
+ // Ollama Cloud tags a signed-in Ollama serves are listed too (Erick,
217
+ // 2026-10-24); the label says they bill a plan.
219
218
  return [id];
220
219
  });
221
220
  }
@@ -349,7 +348,7 @@ export async function probeLocalInjects(env = process.env, fetchImpl = fetch) {
349
348
  id: encodeInjectId(host.id, model),
350
349
  host: host.id,
351
350
  model,
352
- label: `${model} (${host.label})`,
351
+ label: isOllamaCloudModel(model) ? `${model} (${host.label}, plan-billed)` : `${model} (${host.label})`,
353
352
  loaded: loaded.has(model),
354
353
  ...(contextWindow ? { contextWindow } : {}),
355
354
  });
@@ -1,16 +1,19 @@
1
1
  import { classifyEngineFailure, isCredentialDead, mayBeReplyFailure, replyEngineFailure, statedReset } from "./engine-failure.js";
2
- /** At most this many engines take part in one turn: the first and five
3
- * switches. Each engine is tried once per turn anyway; this only bounds a
4
- * long run of failures, which then stops rather than loops. */
5
- export const MAX_ENGINES_PER_TURN = 6;
6
- /** OpenRouter's rule for an error no pattern recognises: fail over while
7
- * nothing has reached the person yet (no text, no tool run), as a
8
- * provider_error. Once part of the answer or an action is out, the turn ends
9
- * as it did. */
2
+ /** The two-try cap: at most this many engines take part in one turn, the
3
+ * first and one switch. Every failed attempt counts toward it, whatever the
4
+ * failure was (a usage limit, a sign-in, a stall, or an error no pattern
5
+ * recognises). When the second engine fails too, the turn stops with its
6
+ * one plain line rather than walking the rest of the ladder. */
7
+ export const MAX_ENGINES_PER_TURN = 2;
8
+ /** What the engine said when it ended a turn failed without any words. */
9
+ export const NO_ANSWER_FAILURE = "The engine ended the turn without an answer.";
10
+ /** Any failure counts: a failed end that no pattern recognises is a
11
+ * provider_error, whether or not part of the answer or a tool run already
12
+ * reached the person (the next engine continues from there, told what was
13
+ * said and which actions ran, as for a usage limit) and whether or not the
14
+ * engine said why. */
10
15
  function unrecognisedFailure(turn) {
11
- if (turn.said || turn.tools || turn.partial || !turn.lastError)
12
- return undefined;
13
- return { kind: "provider_error", message: turn.lastError };
16
+ return { kind: "provider_error", message: turn.lastError ?? NO_ANSWER_FAILURE };
14
17
  }
15
18
  /** Ways a turn ends failed that are not the engine's fault: the person (or
16
19
  * the harness on their behalf) stopped it. Every other failed end moves the
@@ -196,7 +199,7 @@ export const ACCOUNTS_HINT = "Fix it in Settings > AI & accounts.";
196
199
  * engines passed over because they cannot work on the turn's "Works on"
197
200
  * place (`place`, "this computer"). When an account needs the person (a
198
201
  * sign-in, an expired key), the line says where to fix it. */
199
- export function restingLine(engines, formatTime, computer) {
202
+ export function restingLine(engines, formatTime, computer, options = {}) {
200
203
  const seen = new Set();
201
204
  const listed = engines.filter((engine) => !seen.has(engine.label) && seen.add(engine.label));
202
205
  const parts = listed.map((engine) => (engine.keyExpired ? `${engine.label}'s API key has expired`
@@ -208,6 +211,13 @@ export function restingLine(engines, formatTime, computer) {
208
211
  .map((label) => `${label} can't control ${computer.place}`);
209
212
  if (cannot.length)
210
213
  return `No engine that can work on ${computer.place} is free, so this turn stopped: ${[...parts, ...cannot].join("; ")}.${hint}`;
214
+ // The two-try cap, not an empty ladder: say so, with what each engine hit.
215
+ if (options.capped) {
216
+ const count = MAX_ENGINES_PER_TURN === 2 ? "two" : String(MAX_ENGINES_PER_TURN);
217
+ return parts.length
218
+ ? `This turn stopped after ${count} engines failed: ${parts.join(", ")}.${hint}`
219
+ : `This turn stopped after ${count} engines failed.`;
220
+ }
211
221
  if (parts.length === 0)
212
222
  return "No engine is available for this turn, so it stopped.";
213
223
  if (parts.length === 1)
@@ -3,21 +3,11 @@
3
3
  // engine of its ladder that is ready. Everything here is pure except the
4
4
  // snapshot cache, so the server wires it to the live registry and the tests
5
5
  // drive it with plain candidates.
6
- import { BOT_TIERS, DEFAULT_TIER_CLASSES, MODEL_CLASSES, classBelow, isBotTier, toModelClass } from "../shared/engine-tiers.js";
6
+ import { BOT_TIERS, DEFAULT_LADDERS, DEFAULT_TIER_CLASSES, MODEL_CLASSES, classBelow, isBotTier, toModelClass } from "../shared/engine-tiers.js";
7
7
  import { modelHost } from "../shared/engine-choices.js";
8
+ export { DEFAULT_LADDERS };
8
9
  import { subscriptionProviderForDriver } from "../shared/subscription-bots.js";
9
10
  import { ATOMIC_ENGINE_INSTANCE, engineIntent, localModel, modelBilling, modelProvider, paidCandidate, perTokenModel } from "./harness-import/engine-policy.js";
10
- /** Hand-written ladders, used only when no model has a class yet (the
11
- * server always has the bundled baseline, so in practice the generated
12
- * ladders below apply). Pay-per-token engines need no rung: they form the
13
- * automatic last band of every ladder (resolveLadder). */
14
- export const DEFAULT_LADDERS = {
15
- orchestrator: [{ engine: "claudeAgent" }, { engine: "atomic-engine" }, { engine: "codex" }],
16
- engineering: [{ engine: "claudeAgent" }, { engine: "codex" }, { engine: "cursorAgent" }, { engine: "kimiAgent" }],
17
- research: [{ engine: "grokAgent" }, { engine: "antigravityAgent" }, { engine: "kimiAgent" }],
18
- writing: [{ engine: "antigravityAgent" }, { engine: "kimiAgent" }, { engine: "codex" }, { engine: "claudeAgent" }],
19
- routine: [{ engine: "qwenAgent" }, { engine: "kimiAgent" }, { engine: "antigravityAgent" }],
20
- };
21
11
  /** Engines a tier tries first, whatever the class catalog order says: Chibi
22
12
  * runs on Claude (Erick, 2026-10-13: open-weight models broke its
23
13
  * orchestration). Its ladder starts with Claude's best model graded in its
@@ -379,7 +369,7 @@ export function resolveFailover(request) {
379
369
  const ids = candidate.models.options.map((option) => option.id);
380
370
  const multi = ids.some((id) => modelProvider(candidate, id) !== undefined);
381
371
  // (A local host's model, Ollama Cloud tags included, is not one of them:
382
- // Ollama Cloud joins as its own engine, local models only by local rules.)
372
+ // it joins when a ladder names it, else by Ollama Cloud or local rules.)
383
373
  const models = multi ? ids.filter((id) => modelProvider(candidate, id) !== undefined && modelBilling(candidate, id) === "subscription")
384
374
  : candidate.models.default && modelBilling(candidate, candidate.models.default) === undefined ? [candidate.models.default] : [];
385
375
  for (const model of models) {
@@ -410,7 +400,8 @@ export function resolveFailover(request) {
410
400
  return "tried";
411
401
  if (!keys && (keyBased(candidate) || perTokenModel(candidate, model)))
412
402
  return "key-later";
413
- if (request.isLocal?.(candidate, model) && !(request.localAllowed && modelClass === "Añejo"))
403
+ // a ladder-named local model is a normal rung (Erick, 2026-10-24)
404
+ if (!named && request.isLocal?.(candidate, model) && !(request.localAllowed && modelClass === "Añejo"))
414
405
  return "local";
415
406
  const step = named ? ladder.indexOf(entry) : ladder.length;
416
407
  return effective(candidate, model, named ? entry : undefined, step) === modelClass ? undefined : "class";