@chibitek/chibi 0.1.157 → 0.1.160

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 (107) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-BkYbog6D.js → abnfDiagram-O67JEVCF-CpuRvzQ3.js} +1 -1
  2. package/dist/assets/{arc-QwTuMsxp.js → arc-ko1e0R6h.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-GdicO2PQ.js → architectureDiagram-NJMV4G6O-OZ3SsOOa.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-Dkr29TFF.js → blockDiagram-BEXU5L5S-DLRkBkjs.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-Dz_3dstx.js → c4Diagram-YGBWAQC7-e8Q3qMnw.js} +1 -1
  6. package/dist/assets/channel-Lp7erhx-.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-CGsfu-PI.js → chunk-2Q5K7J3B-hCDqaM4Q.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-CDqI37_6.js → chunk-3FUC2YCW-Dy3Pahu-.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-DnmSuyTb.js → chunk-5VM5RSS4-QMK3iFBF.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-Dh7tXcJT.js → chunk-GTNCS2PH-6fyp9ERp.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-D_SM1ZrY.js → chunk-GWA4HPMP-DWvOhTaO.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-DmeMuTsI.js → chunk-JWPE2WC7-DgohMW2S.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-R1UT9b5f.js → chunk-NETBCI7D-sRGeD2Y7.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-DpPfU1yO.js → chunk-WEXAMYUT-CZslD_VF.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-DlihfTkM.js → chunk-XXDRQBXY-D7ocO3VN.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-BlTV6lSC.js → classDiagram-v2-NBCMYWYE-T1lLqHHW.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-Br1kPFWw.js → cose-bilkent-JH36ORCC-BdeLiJr3.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-BqPp3ZMd.js → cynefin-OW5HDTMX-BydCA0pv.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-DFfs_k8-.js → cynefinDiagram-VND7K2PF-DBxckjwQ.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-Ch3xIJA7.js → dagre-6A5THRUB-CAsvq6Zk.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-DIHUD87N.js → diagram-22UHCM2B-DTwH3rA_.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-CAgQQdeF.js → diagram-3UASUU5V-BNYj1bey.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-9HVKodrI.js → diagram-ATOU4E4O-CihwYFxb.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-DU2kDq_h.js → diagram-CDSNMT55-nouwk3s2.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-D-qTAhHD.js → diagram-MLGK6HIB-BoKewJeA.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-C31SnI9P.js → diagram-MPIPVDR6-CrE1m9DQ.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-BHrdiBVd.js → ebnfDiagram-ZINNZB2B-DRj0aU4Y.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-DWyljRo5.js → elk-276RUBZZ-Do0JzCG5.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-DyY59XPp.js → erDiagram-OPXOYQCR-CRsI9pXb.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-BevMIeAy.js → flowDiagram-KWPJA3E3-dtD6wm3G.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-Ct18WHf4.js → ganttDiagram-FUAMR5RP-Cn7KyLCo.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-yhixO1N3.js → gitGraphDiagram-X574FWY7-CGH_VWzq.js} +1 -1
  33. package/dist/assets/index-B2fJMh8N.css +1 -0
  34. package/dist/assets/{index-ziyH_uer.js → index-BIcziktH.js} +253 -250
  35. package/dist/assets/{index-BEJ8EKQt.js → index-Bk8iAodB.js} +1 -1
  36. package/dist/assets/{infoDiagram-VRGFBTTK-CYa-r_S5.js → infoDiagram-VRGFBTTK-CQKsYTCL.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CnxwM7gY.js → ishikawaDiagram-OU5B5YK6-D5catDe0.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-RKQfXBrp.js → journeyDiagram-ZHPQQLJL-KjY8mki9.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-CSlRDIF6.js → kanban-definition-PNTS6WVX-BKCyJtge.js} +1 -1
  40. package/dist/assets/{linear-Ch19KVyt.js → linear-B-_obUkq.js} +1 -1
  41. package/dist/assets/{mermaid.core-Bek7I_t6.js → mermaid.core-DlSzuDKt.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-B_LiP8su.js → mindmap-definition-NLK3R4M7-BFQSaYKh.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-DhzQeiZf.js → pegDiagram-GJSIUBJH-l51izlev.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-D-BYNKHS.js → pieDiagram-5QR66LMP-6NEXRoZX.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-DnCm0heP.js → quadrantDiagram-O4NWA36T-BCC9XlOe.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-COqgYnhn.js → railroadDiagram-XR7U4H2S-Dd4TmJ86.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-Dvt72ajf.js → requirementDiagram-PLB6GJNP-C1E5DaDb.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-CisC9nbp.js → sankeyDiagram-IPEJSGJF-Ci6r7abb.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-BO8xqvxa.js → sequenceDiagram-PO4LG4MO-BJ-I6WOh.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-DLk1jBzf.js → sizeCapture-INFHLROL-Cs5QbLOP.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-C_wutSE7.js → stateDiagram-v2-GCMORJYK-Bw--rdvq.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-hqo1UlEi.js → swimlanes-2SLR337P-B1O9IEz5.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-C6wGgYKP.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-C_DJldPg.js → timeline-definition-EJHVYXUP-CTZ7DXaw.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-CsidVXMA.js → usecaseDiagram-POWQR4AR-DjwgNG0H.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-Bs-IIqn_.js → vennDiagram-UO4OBE2U-DrdJG55h.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-CXEUyxaj.js → wardleyDiagram-VNRHLVJA-HVq776nz.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-B9qvmm4P.js → xychartDiagram-PMCCYNJV-DPf9VEua.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/connector-proxy.js +205 -0
  61. package/dist-server/container-mcp.js +150 -96
  62. package/dist-server/drivers/agents-proxy.js +81 -81
  63. package/dist-server/index.js +7054 -6099
  64. package/dist-server/openmausbot.js +940 -727
  65. package/dist-server/pair-cli.js +940 -727
  66. package/dist-server/server/attachment-mimes.js +58 -0
  67. package/dist-server/server/attachments.js +28 -67
  68. package/dist-server/server/avatar-image.js +1 -0
  69. package/dist-server/server/avatar-subscription.js +74 -0
  70. package/dist-server/server/cli.js +5 -4
  71. package/dist-server/server/colima-docker.js +136 -0
  72. package/dist-server/server/composio.js +1051 -0
  73. package/dist-server/server/connector-proxy.js +249 -0
  74. package/dist-server/server/container-computer.js +31 -63
  75. package/dist-server/server/container-mcp.js +5 -0
  76. package/dist-server/server/drivers/claude-message.js +12 -0
  77. package/dist-server/server/drivers/claude.js +9 -12
  78. package/dist-server/server/drivers/codex-image.js +141 -0
  79. package/dist-server/server/drivers/codex.js +6 -9
  80. package/dist-server/server/drivers/openai-chat.js +13 -2
  81. package/dist-server/server/engine-failover.js +62 -12
  82. package/dist-server/server/engine-failure.js +2 -2
  83. package/dist-server/server/harness-import/history.js +5 -5
  84. package/dist-server/server/harness-import/test-fixtures.js +1 -1
  85. package/dist-server/server/index.js +75 -36
  86. package/dist-server/server/mcp-public-registry.js +325 -0
  87. package/dist-server/server/message-reactions.js +11 -0
  88. package/dist-server/server/model-evidence-context.js +18 -0
  89. package/dist-server/server/model-evidence.js +145 -0
  90. package/dist-server/server/port-in-use.js +21 -0
  91. package/dist-server/server/session-fallback.js +76 -0
  92. package/dist-server/server/vault/routes.js +43 -0
  93. package/dist-server/server/vault/runtime.js +48 -0
  94. package/dist-server/server/vault/store.js +177 -0
  95. package/dist-server/shared/connector-availability.js +12 -0
  96. package/dist-server/shared/desktop-private-port.js +1 -0
  97. package/dist-server/shared/emoji-catalog.json +1 -0
  98. package/dist-server/shared/emoji.js +27 -0
  99. package/dist-server/shared/mascot-bodies.js +9 -1
  100. package/dist-server/shared/model-evidence.js +25 -0
  101. package/dist-server/shared/subscription-bots.js +3 -0
  102. package/dist-server/shared/vault.js +59 -0
  103. package/dist-server/vps-container-mcp.js +86 -84
  104. package/package.json +1 -1
  105. package/dist/assets/channel-D2y0i2ps.js +0 -1
  106. package/dist/assets/index-DGWeJcck.css +0 -1
  107. package/dist/assets/swimlanesDiagram-TC7HE7FX-D15JqTL9.js +0 -8
@@ -42,7 +42,7 @@ export function createOpenAIChatRuntime(options) {
42
42
  turnId,
43
43
  createdAt: new Date().toISOString(),
44
44
  });
45
- const complete = async (messages, model, stream, signal, onDelta, tools = []) => {
45
+ const complete = async (messages, model, stream, signal, onDelta, tools = [], onIdentity) => {
46
46
  // Idle timer that is renewed on every received chunk during streaming
47
47
  const timeoutController = new AbortController();
48
48
  let idleTimer = null;
@@ -77,6 +77,7 @@ export function createOpenAIChatRuntime(options) {
77
77
  const bodyError = providerError(json);
78
78
  if (bodyError)
79
79
  throw new ChatProtocolError(`provider returned a completion error: ${bodyError.slice(0, 200)}`);
80
+ onIdentity?.(typeof json.model === "string" ? json.model : null, typeof json.id === "string" ? json.id : undefined);
80
81
  const message = json.choices?.[0]?.message;
81
82
  if (!message || !object(message) || !["content", "reasoning_content", "reasoning", "reasoning_details", "tool_calls", "function_call"].some((key) => key in message)) {
82
83
  throw new ChatProtocolError("provider returned no completion message");
@@ -141,6 +142,7 @@ export function createOpenAIChatRuntime(options) {
141
142
  const chunkError = providerError(chunk);
142
143
  if (chunkError)
143
144
  throw new ChatProtocolError(`provider returned a streaming completion error: ${chunkError.slice(0, 200)}`);
145
+ onIdentity?.(typeof chunk.model === "string" ? chunk.model : null, typeof chunk.id === "string" ? chunk.id : undefined);
144
146
  const choice = chunk.choices?.find((row) => row.index === undefined || row.index === 0);
145
147
  const delta = choice?.delta;
146
148
  if (object(delta))
@@ -329,10 +331,19 @@ export function createOpenAIChatRuntime(options) {
329
331
  emit({ ...base(turn.threadId, turnId), type: "content.delta", streamKind, delta: visible });
330
332
  };
331
333
  try {
334
+ const callId = newId();
335
+ emit({ ...base(turn.threadId, turnId), type: "model.call", callId });
336
+ let previousIdentity = "";
332
337
  completion = await complete(messages, model, true, abort.signal, (text, streamKind) => {
333
338
  streamed = true;
334
339
  delta(text, streamKind);
335
- }, tools.definitions);
340
+ }, tools.definitions, (reportedModel, responseId) => {
341
+ const identity = JSON.stringify([reportedModel, responseId]);
342
+ if (identity === previousIdentity)
343
+ return;
344
+ previousIdentity = identity;
345
+ emit({ ...base(turn.threadId, turnId), type: "model.evidence", callId, model: reportedModel, responseId, level: reportedModel ? "response-reported" : "unknown" });
346
+ });
336
347
  delta("", "assistant_text", true);
337
348
  delta("", "reasoning_text", true);
338
349
  break;
@@ -1,16 +1,53 @@
1
+ // Silent automatic failover (docs/chibitek/engine-tiers.md, step 2).
2
+ //
3
+ // A turn whose engine runs out of its usage allowance, turns out to be
4
+ // signed out, goes fully silent, or fails in any other way, is not failed:
5
+ // the engine rests and the same turn runs on the next engine of the bot's
6
+ // ladder at the same level, at most two tries per turn. No chat message is
7
+ // added; the client shows a small banner that fades (src/lib/engine-switch.ts).
8
+ //
9
+ // Modelled on OpenRouter's routing (docs/chibitek/engine-tiers.md): any
10
+ // error is a reason to try the next candidate, not only the ones we have
11
+ // learned to recognise. Recognised failures (a usage limit, a failed or
12
+ // dead sign-in, a stall, a rate limit, a reasonless 400) rest the engine
13
+ // for as long as that cause lasts; anything else is `provider_error`, which
14
+ // rests it briefly (a circuit breaker) so it is not hammered. Every error an
15
+ // engine reports while its turn is watched is held, so the person never sees
16
+ // a provider's raw words for a failure the next engine recovers from.
17
+ // This module is the part that watches a turn's events:
18
+ //
19
+ // - While a turn is tracked, the engine's own report of the limit (a
20
+ // runtime.error, or the text a CLI prints in place of a reply, which the
21
+ // drivers mark `synthetic`) is held back instead of delivered. So is a
22
+ // reply that starts the way an agent's billing error does (Hermes answers
23
+ // "Billing or credits exhausted: HTTP 402: ..." as ordinary text and ends
24
+ // the turn as a success): it waits until the reply is complete, and if it
25
+ // is that error, with no tool run, the turn counts as failed.
26
+ // - When the turn completes failed with a held (or recognisable) limit,
27
+ // `onFailure` decides: it either moves the turn to another engine, and
28
+ // returns the one record that says so (turn.engine_failover), or it stops
29
+ // the turn with one plain line naming the resting engines.
30
+ // - A turn that ends any other way gets everything it held, in order.
31
+ //
32
+ // The server wires `onFailure` to the ladder, the resting store and
33
+ // startTurn (server/index.ts); everything here is pure bookkeeping.
34
+ import { newEventId } from "./contracts.js";
1
35
  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. */
36
+ /** The two-try cap: at most this many engines take part in one turn, the
37
+ * first and one switch. Every failed attempt counts toward it, whatever the
38
+ * failure was (a usage limit, a sign-in, a stall, or an error no pattern
39
+ * recognises). When the second engine fails too, the turn stops with its
40
+ * one plain line rather than walking the rest of the ladder. */
41
+ export const MAX_ENGINES_PER_TURN = 2;
42
+ /** What the engine said when it ended a turn failed without any words. */
43
+ export const NO_ANSWER_FAILURE = "The engine ended the turn without an answer.";
44
+ /** Any failure counts: a failed end that no pattern recognises is a
45
+ * provider_error, whether or not part of the answer or a tool run already
46
+ * reached the person (the next engine continues from there, told what was
47
+ * said and which actions ran, as for a usage limit) and whether or not the
48
+ * engine said why. */
10
49
  function unrecognisedFailure(turn) {
11
- if (turn.said || turn.tools || turn.partial || !turn.lastError)
12
- return undefined;
13
- return { kind: "provider_error", message: turn.lastError };
50
+ return { kind: "provider_error", message: turn.lastError ?? NO_ANSWER_FAILURE };
14
51
  }
15
52
  /** Ways a turn ends failed that are not the engine's fault: the person (or
16
53
  * the harness on their behalf) stopped it. Every other failed end moves the
@@ -196,7 +233,7 @@ export const ACCOUNTS_HINT = "Fix it in Settings > AI & accounts.";
196
233
  * engines passed over because they cannot work on the turn's "Works on"
197
234
  * place (`place`, "this computer"). When an account needs the person (a
198
235
  * sign-in, an expired key), the line says where to fix it. */
199
- export function restingLine(engines, formatTime, computer) {
236
+ export function restingLine(engines, formatTime, computer, options = {}) {
200
237
  const seen = new Set();
201
238
  const listed = engines.filter((engine) => !seen.has(engine.label) && seen.add(engine.label));
202
239
  const parts = listed.map((engine) => (engine.keyExpired ? `${engine.label}'s API key has expired`
@@ -208,9 +245,22 @@ export function restingLine(engines, formatTime, computer) {
208
245
  .map((label) => `${label} can't control ${computer.place}`);
209
246
  if (cannot.length)
210
247
  return `No engine that can work on ${computer.place} is free, so this turn stopped: ${[...parts, ...cannot].join("; ")}.${hint}`;
248
+ // The two-try cap, not an empty ladder: say so, with what each engine hit.
249
+ if (options.capped) {
250
+ const count = MAX_ENGINES_PER_TURN === 2 ? "two" : String(MAX_ENGINES_PER_TURN);
251
+ return parts.length
252
+ ? `This turn stopped after ${count} engines failed: ${parts.join(", ")}.${hint}`
253
+ : `This turn stopped after ${count} engines failed.`;
254
+ }
211
255
  if (parts.length === 0)
212
256
  return "No engine is available for this turn, so it stopped.";
213
257
  if (parts.length === 1)
214
258
  return `${parts[0]}, and no other engine at the same level is free, so this turn stopped.${hint}`;
215
259
  return `Every engine this bot can use is resting, so this turn stopped: ${parts.join(", ")}.${hint}`;
216
260
  }
261
+ /** A new harness event about the same provider turn as `event`. */
262
+ export const eventBase = (event) => ({
263
+ eventId: newEventId(), provider: event.provider, threadId: event.threadId, createdAt: new Date().toISOString(),
264
+ ...(event.providerInstanceId ? { providerInstanceId: event.providerInstanceId } : {}), ...(event.turnId ? { turnId: event.turnId } : {}),
265
+ });
266
+ export const restTime = (at) => new Date(at).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
@@ -160,9 +160,9 @@ export function classifyEngineFailure(text, now = Date.now()) {
160
160
  return null;
161
161
  }
162
162
  // Hermes returns billing and expired-login refusals as successful reply text.
163
- const REPLY_FAILURE_HEADS = ["login expired", "error: nonretriableerror:", "billing or credits exhausted", "http 402:", "error: http 402:", "api error: http 402:"];
163
+ const REPLY_FAILURE_HEADS = ["login expired", "error: nonretriableerror:", "billing or credits exhausted", "http 402:", "error: http 402:", "api error: http 402:", "usage limit reached"];
164
164
  const REPLY_FAILURE = [
165
- /^billing or credits exhausted\b/i,
165
+ /^billing or credits exhausted\b|^usage limit reached\b[\s\S]*\b(?:quota|limit will reset|resets? (?:in|at|on))\b/i, // the second: a quota page as the reply (2026-10-26)
166
166
  // A bare "HTTP 402:" at the head counts only with the provider's own
167
167
  // billing words after it.
168
168
  /^(?:(?:api )?error:\s*)?http 402:[\s\S]*\b(?:positive balance|insufficient (?:balance|credits?|funds)|add (?:credits|balance|funds)|top-?up|credits? (?:exhausted|depleted))\b/i,
@@ -11,7 +11,7 @@ import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import { DatabaseSync } from "node:sqlite";
13
13
  import { pathToFileURL } from "node:url";
14
- import { attachmentBytesUsed, ATTACHMENTS_DIR, ATTACHMENTS_MAX_BYTES, deleteAttachment, extensionForFileMime, extensionForMime, FILE_MAX_BYTES, IMAGE_MAX_BYTES, saveFile, saveImage, } from "../attachments.js";
14
+ import { attachmentBytesAvailable, ATTACHMENTS_DIR, deleteAttachment, extensionForFileMime, extensionForMime, FILE_MAX_BYTES, IMAGE_MAX_BYTES, saveFile, saveImage, } from "../attachments.js";
15
15
  import { redactSecretsInText } from "../redact.js";
16
16
  import { existingBots, importIdentity, mergedBotOf, sourceRefFor } from "./apply.js";
17
17
  import { GROKBOT_HISTORY_ALLOWLIST } from "./grokbot.js";
@@ -31,7 +31,7 @@ const MAX_STORE_BYTES = 2 * 1024 * 1024 * 1024;
31
31
  /** The real attachment directory, through the same save calls uploads use. */
32
32
  export function historyAttachmentStore() {
33
33
  return {
34
- usedBytes: attachmentBytesUsed,
34
+ availableBytes: attachmentBytesAvailable,
35
35
  has(uploadId, extension) {
36
36
  try {
37
37
  return statSync(join(ATTACHMENTS_DIR, `${uploadId}${extension}`)).isFile();
@@ -421,7 +421,7 @@ export function planBotHistory(sourceId, entries, ctx) {
421
421
  hidden: cleaner.hidden(),
422
422
  };
423
423
  }
424
- export const defaultHistoryBudget = (usedBytes) => Math.max(0, ATTACHMENTS_MAX_BYTES - usedBytes - HISTORY_QUOTA_MARGIN_BYTES);
424
+ export const defaultHistoryBudget = (availableBytes) => Math.max(0, availableBytes - HISTORY_QUOTA_MARGIN_BYTES);
425
425
  /** Which files to copy: images first, then documents, newest first, until
426
426
  * the budget is spent. A file already in Atomic costs nothing. */
427
427
  export function allocateFiles(input, budget, stored) {
@@ -608,7 +608,7 @@ const storedCheck = (store) => (file) => {
608
608
  export function previewHistory(root, result, deps, options) {
609
609
  const source = options.source ?? GROKBOT_HISTORY;
610
610
  const targets = historyTargets(result, deps.bots());
611
- const budget = options.budget ?? defaultHistoryBudget(deps.attachments.usedBytes());
611
+ const budget = options.budget ?? defaultHistoryBudget(deps.attachments.availableBytes());
612
612
  const empty = { threads: 0, messages: 0, files: { copied: 0, copiedBytes: 0, skipped: 0, skippedBytes: 0 } };
613
613
  if (!options.requested) {
614
614
  return { requested: false, available: targets.filter((target) => source.available(root, target.sourceId, options.fs)).length, bots: [], totals: empty, budgetBytes: budget };
@@ -698,7 +698,7 @@ export async function applyHistory(root, result, deps, options = {}) {
698
698
  out.hiddenMessages += plan.hidden;
699
699
  }
700
700
  }
701
- const budget = options.budget ?? defaultHistoryBudget(deps.attachments.usedBytes());
701
+ const budget = options.budget ?? defaultHistoryBudget(deps.attachments.availableBytes());
702
702
  const stored = storedCheck(deps.attachments);
703
703
  const allocation = allocateFiles(work.flatMap((item) => item.threads.flatMap(({ thread }) => thread.messages.flatMap((message) => message.files))), budget, stored);
704
704
  out.files.tooLarge = allocation.tooLarge.size;
@@ -276,7 +276,7 @@ export function fakeHistoryDeps(options = {}) {
276
276
  },
277
277
  deleteThread: (_botId, threadId) => state.threads.delete(threadId),
278
278
  attachments: {
279
- usedBytes: () => state.used,
279
+ availableBytes: () => 512 * 1024 * 1024 - state.used,
280
280
  has: (uploadId, extension) => state.files.has(`/attachments/${uploadId}${extension}`),
281
281
  saveImage: (bytes, mime, uploadId) => {
282
282
  const path = pathFor(uploadId, mime);
@@ -1,3 +1,5 @@
1
+ import { createModelExpectation, ModelEvidenceLedger } from "./model-evidence-context.js";
2
+ import { reactToMessage } from "./message-reactions.js";
1
3
  import { hasUnboundGoalGuard, goalGuardForEvent, addGoalGuard, removeGoalGuard, runPersonalGoalTurn, updatePersonalGoalReceipt } from "./personal-goal-turn.js";
2
4
  import { PersonalGoalRuntime, receiveGoalCommand } from "./personal-goal-runtime.js";
3
5
  import { personalGoalInstructions } from "./personal-goals.js";
@@ -46,7 +48,8 @@ import { validateBotCwd } from "./bot-cwd.js";
46
48
  import { ATTACHMENTS_DIR, attachmentExists, cleanupStaleAttachmentPartials, deleteAttachment, extensionForMime, FILE_MAX_BYTES, IMAGE_MAX_BYTES, readAttachment, saveFile, saveImage, saveImageUpload, validateAttachmentUploadId, } from "./attachments.js";
47
49
  import { companyCheck, companyContext } from "./company-document-check.js";
48
50
  import { messageFileDisposition, messageFileDownloadName, messageAttachmentName, messageFileRoots, messageImageTargetAt, messageReferencesFile, openMessageFile, } from "./message-file.js";
49
- import { avatarGenerationRequestSchema, avatarGenerationStateMatches, generateAvatarImage, avatarImageStatus, snapshotAvatarGenerationState, } from "./avatar-image.js";
51
+ import { avatarGenerationRequestSchema, avatarGenerationStateMatches, avatarImageStatus, snapshotAvatarGenerationState } from "./avatar-image.js";
52
+ import { generateRoutedAvatar, installAvatarSubscriptionRoute } from "./avatar-subscription.js";
50
53
  import { fitsOnOneLine, parseBotProfilePatch } from "./bot-profile.js";
51
54
  import { groupTurnCwd } from "./room-cwd.js";
52
55
  import { RepoRegistry } from "./repos.js";
@@ -68,7 +71,7 @@ import { turnTimingFinish, turnTimingMark, turnTimingStart } from "./turn-timing
68
71
  import { ATOMIC_TEAM_PACKAGE_ID, BUILTIN_CHIBI_REFUSALS, builtinChibiPatchRefusal } from "./builtin-chibi.js";
69
72
  import { canAccessTeam, canContactTeamOf, canManagePeer, canReachPeer, canReachTeam, coordinatorSupervises, peerAllowed, peerName, peerRosterSystemPrompt, peerStatus, peerStatusWords, reachablePeers, reachesEveryTeam, roomBoundaryHolds, teamStatusPrompt, PEER_ROSTER_MAX, resolveTeammate, roomPeerRosterSystemPrompt, rosterAbout, roomRosterLine, PEER_ACCESS_HELP } from "./peer-roster.js";
70
73
  import { openMausStatusSystemPrompt } from "./openmaus-status-capsule.js";
71
- import { containerComputerAction, managedImageNeedsUpgrade, containerComputerExists, containerComputerFrame, containerComputerMcp, containerComputerScreenshot, containerComputerStatus, recentContainerComputerStatus, containerRuntimeStatus, ensureContainerRuntime, localVmRecreatableOnDemand, localVmReplaceableWhenStopped, perBotLocalVmTarget, SHARED_LOCAL_VM_TARGET, setupCommands, } from "./container-computer.js";
74
+ import { containerComputerAction, managedImageNeedsUpgrade, containerComputerExists, containerComputerFrame, containerComputerMcp, containerComputerScreenshot, containerComputerStatus, recentContainerComputerStatus, containerRuntimeStatus, ensureContainerRuntime, containerRuntimeError, localVmRecreatableOnDemand, localVmReplaceableWhenStopped, perBotLocalVmTarget, SHARED_LOCAL_VM_TARGET, setupCommands, } from "./container-computer.js";
72
75
  import { ensureDirs, instanceConfigs, loadConfig, providerReloadKeys, localVmMaxInstances, localVmAutoStartRuntime, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, threadEventLogMaxBytes, maxConcurrentBotThreads, threadEventLogRetentionDays, saveConfig, showToolCallsEnabled, claudeUserMcpEnabled, userMcpEnabledFor, addonEnabled, skillAuthoringEnabled, sharedComputersEnabled, teamRoomsEnabled, builtInBrowserEnabled, llmThreadTitlesEnabled, onDeviceHelpersEnabled, browserProfileReplacementConflict, browserProfilePartitionTarget, syncCredentialEnv, withInstanceCli, persistableInstanceConfigs, vpsSshAlias, browserEngineAttachCdpUrl, DATA_DIR, EVENTS_DIR, NATIVE_DIR, customMcpServers, roomHandoffLimits, onConfigSaved, accountBotGrantPolicy, } from "./config.js";
73
76
  import { sweepThreadEventLogs } from "./thread-retention.js";
74
77
  import { GodModeSwitch } from "./god-mode.js";
@@ -263,6 +266,7 @@ import { createProviderAccountRoutes } from "./routes/provider-accounts.js";
263
266
  import { accountStops, injectedModel, restScope } from "./injected-model-rest.js";
264
267
  import { accountProvider } from "../shared/provider-accounts.js";
265
268
  import { createAccountRoutes } from "./routes/accounts.js";
269
+ import { installNativeVault } from "./vault/runtime.js";
266
270
  import { OAuthAccounts, takeOAuthClients } from "./oauth-accounts.js";
267
271
  import { DesktopAccountVault, FileAccountVault, UnavailableAccountVault, takeAccountsFromEnvironment } from "./oauth-account-vault.js";
268
272
  import { accountConnectorNames } from "./claude-account-connectors.js";
@@ -303,7 +307,8 @@ import { createNexusMemoryRoutes } from "./routes/nexus-memory.js";
303
307
  import { createBoardRoutes } from "./routes/board.js";
304
308
  import { readBoardFromNexus } from "./board/nexus-board.js";
305
309
  import { LocalModelInventory, localModePlacement, machineMemory } from "./local-models-mode.js";
306
- import { FailoverGate, MAX_ENGINES_PER_TURN, continuationNote, restingLine } from "./engine-failover.js";
310
+ import { eventBase, restTime, FailoverGate, MAX_ENGINES_PER_TURN, continuationNote, restingLine } from "./engine-failover.js";
311
+ import { enterSessionFallback, onSessionFallback, ownOrFallback, sessionFallbackSpentLine, withoutSubscription } from "./session-fallback.js";
307
312
  import { TURN_ERROR_CATEGORIES } from "../shared/runtime-events.js";
308
313
  import { EngineProbes, PROBE_PROMPT, isProbeThread, probeThreadId } from "./engine-probe.js";
309
314
  // [jev-verifier] opt-in Jev answer checks after a model switch (server/jev).
@@ -485,6 +490,7 @@ function customDomainStatus() {
485
490
  };
486
491
  }
487
492
  const registry = new ProviderRegistry(BUILT_IN_DRIVERS);
493
+ installAvatarSubscriptionRoute(() => registry.instances());
488
494
  // Engines installed from Settings live under the data directory and win over
489
495
  // any other copy on PATH.
490
496
  registerEnginesBinDir();
@@ -1154,9 +1160,14 @@ if (!hostedModels) {
1154
1160
  // its add-on's capability, so it loads only while the add-on applies.
1155
1161
  const bundledSkills = [...loadBundledSkills(), ...loadAddonSkillLibraries()];
1156
1162
  const availableSkills = () => mergeSkills(bundledSkills, loadUserSkills(join(DATA_DIR, "skills")));
1163
+ // Electron's utility-process parent port is private to the desktop main
1164
+ // process. It carries desktop-only messages (approval and browser control,
1165
+ // phone secrets, the account vault) without putting them in the renderer.
1166
+ // Plain Node/dev launches have no parentPort.
1157
1167
  // SAFETY: Electron's utility-process runtime is the only environment that
1158
1168
  // supplies parentPort; plain Node intentionally leaves it absent.
1159
1169
  const utilityParentPort = process.parentPort;
1170
+ installNativeVault(DATA_DIR, utilityParentPort, () => desktopMutationToken, req => authorizedInternalCapability(req.headers.authorization) !== null);
1160
1171
  function postDesktopPrivateMessage(message) {
1161
1172
  if (!utilityParentPort)
1162
1173
  return false;
@@ -3045,12 +3056,19 @@ function turnEngineChoice(botId, threadId, needs, runOn) {
3045
3056
  if (hostedModels || !profile || !task)
3046
3057
  return undefined;
3047
3058
  if (isSubscriptionBot(profile))
3048
- return subscriptionTurnEngineChoice(profile, threadId);
3059
+ return ownOrFallback(threadId, () => subscriptionTurnEngineChoice(profile, threadId), () => sessionFallbackChoice(profile, threadId, needs, runOn), (stop) => stop instanceof SubscriptionStop, (stop) => new SubscriptionStop(sessionFallbackSpentLine(stop.message)));
3049
3060
  if (isPersonalBot(profile))
3050
3061
  return personalTurnEngineChoice(profile, threadId, needs, runOn);
3051
3062
  const projected = store.projectBotForTask(botId, threadId);
3052
3063
  return projected ? ladderTurnEngineChoice(profile, projected, threadId, needs, runOn) : undefined;
3053
3064
  }
3065
+ /** A company subscription bot's conversation on another provider's subscription, same class (server/session-fallback.ts). */
3066
+ function sessionFallbackChoice(profile, threadId, needs, runOn) {
3067
+ const projected = store.projectBotForTask(profile.id, threadId);
3068
+ const choice = isPersonalBot(profile) || !projected ? undefined
3069
+ : ladderTurnEngineChoice(withoutSubscription(profile), withoutSubscription(projected), threadId, needs, runOn, true);
3070
+ return choice && choice.selection.instanceId !== projected.modelSelection.instanceId ? { selection: choice.selection, persist: false } : undefined;
3071
+ }
3054
3072
  /** A room member's engine at turn start, by turnEngineChoice's rules for
3055
3073
  * the bot's own default (a room turn has no task of its own, so the bot's
3056
3074
  * engine mode decides): resting and signed-out engines passed over, a
@@ -3066,7 +3084,7 @@ function roomTurnEngineChoice(bot, threadId) {
3066
3084
  : ladderTurnEngineChoice(bot, projected, threadId);
3067
3085
  return choice && { selection: choice.selection, persist: false };
3068
3086
  }
3069
- function ladderTurnEngineChoice(profile, projected, threadId, needs, runOn) {
3087
+ function ladderTurnEngineChoice(profile, projected, threadId, needs, runOn, subscriptionsOnly = false) {
3070
3088
  const candidates = engineSnapshots.current();
3071
3089
  if (!candidates)
3072
3090
  return undefined;
@@ -3075,7 +3093,7 @@ function ladderTurnEngineChoice(profile, projected, threadId, needs, runOn) {
3075
3093
  const { ladder } = plan;
3076
3094
  const computer = turnComputer(projected, threadId, runOn);
3077
3095
  const context = { candidates, needsDelegation: needsDelegation(tier, profile), computer, refuse: engineTierRefusal(projected), resting: engineResting,
3078
- keyBandPaused: payPerUse.pausedUntil() !== undefined };
3096
+ keyBandPaused: subscriptionsOnly || payPerUse.pausedUntil() !== undefined };
3079
3097
  const local = (resolution) => localModeTurn(resolution, { tier, context, needs, prefer: [projected.modelSelection.instanceId] });
3080
3098
  const wider = (tried, modelClass) => resolveFailover({
3081
3099
  ladder, classLadder: (cls) => classLadder(classedModels(), cls), failed: projected.modelSelection, context, tried,
@@ -3550,6 +3568,9 @@ const roomFailoverTurns = new Map();
3550
3568
  * default (a resting engine's stand-in, a failover): the ledger books it. */
3551
3569
  const roomTurnSelections = new Map();
3552
3570
  const turnEngineOverrides = new Map();
3571
+ const modelEvidence = new ModelEvidenceLedger(DATA_DIR);
3572
+ const modelExpectation = createModelExpectation({ get store() { return store; }, get registry() { return registry; }, turnEngineOverrides, roomTurnSelections,
3573
+ speakerBot: threadId => groupSpeakers.get(threadId)?.botId, enginePlan, botClassChoices });
3553
3574
  const failoverGate = new FailoverGate((turn, completion) => engineFailover(turn, completion));
3554
3575
  /** An approval shows a shell command as the model meant it: the RTK add-on's
3555
3576
  * launcher, which engines that take RTK as instructions put in front of
@@ -3568,9 +3589,16 @@ bus.setGate((published) => {
3568
3589
  return [];
3569
3590
  }
3570
3591
  const event = withoutRtkInSummary(published);
3592
+ if (!shouldIgnoreProviderEvent(event))
3593
+ modelEvidence.observe(event, () => modelExpectation(event.threadId, event.providerInstanceId, event.provider));
3571
3594
  // [jev-verifier] note a failover turn that finished, before the gate forgets it.
3572
3595
  const jevCandidate = jevAnswerCheckCandidate(event);
3573
3596
  const delivered = failoverGate.filter(event);
3597
+ // Gate-generated switches never re-enter publish; preserve their evidence too.
3598
+ for (const out of delivered)
3599
+ if (out.type === "turn.engine_failover" && out !== event) {
3600
+ modelEvidence.observe(out, () => modelExpectation(out.threadId, out.providerInstanceId, out.provider));
3601
+ }
3574
3602
  if (jevCandidate)
3575
3603
  setImmediate(() => void jevAnswerCheck(jevCandidate));
3576
3604
  for (const out of delivered)
@@ -3589,12 +3617,6 @@ function noteEngineBack(event) {
3589
3617
  const model = selection?.instanceId === instanceId ? selection.model : undefined;
3590
3618
  engineRests.noteBack(instanceId, candidate ? engineAccount(candidate) : undefined, candidate ? restScope(candidate, model) : undefined);
3591
3619
  }
3592
- /** A new harness event about the same provider turn as `event`. */
3593
- const eventBase = (event) => ({
3594
- eventId: newEventId(), provider: event.provider, threadId: event.threadId, createdAt: new Date().toISOString(),
3595
- ...(event.providerInstanceId ? { providerInstanceId: event.providerInstanceId } : {}), ...(event.turnId ? { turnId: event.turnId } : {}),
3596
- });
3597
- const restTime = (at) => new Date(at).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
3598
3620
  /** Rest a refused engine and continue on an eligible engine, or show an actionable line. */
3599
3621
  function engineFailover(turn, completion) {
3600
3622
  const failure = turn.failure;
@@ -3812,14 +3834,19 @@ function failoverPick(turn, profile, projected, candidates, failedCandidate, res
3812
3834
  const computer = projected ? turnComputer(projected, turn.threadId, attempt.runOn) : undefined;
3813
3835
  // A subscription bot fails over only to another account of its own
3814
3836
  // provider on its side of the personal line; with none left it stops with
3815
- // its one line (and the client offers Chibi). Never another provider.
3837
+ // its one line, or a company bot's own conversation takes its session fallback (server/session-fallback.ts).
3816
3838
  const subscriptionMove = isSubscriptionBot(profile)
3817
3839
  ? tried.size >= MAX_ENGINES_PER_TURN || !projected
3818
3840
  ? { line: subscriptionLine(profile, candidates) }
3819
3841
  : subscriptionPlacement({ profile, projected, candidates, tried: new Set([...tried, turn.instanceId]), auto: attempt.persist, modelClass: neededClass })
3820
3842
  : undefined;
3821
- if (subscriptionMove && !subscriptionMove.choice)
3843
+ const fallback = subscriptionMove && !subscriptionMove.choice && projected && !isPersonalBot(profile) && tried.size < MAX_ENGINES_PER_TURN && store.taskByThread(profile.id, turn.threadId);
3844
+ if (fallback && !enterSessionFallback(turn.threadId, false))
3845
+ return { line: sessionFallbackSpentLine(subscriptionMove.line) };
3846
+ if (subscriptionMove && !subscriptionMove.choice && !fallback)
3822
3847
  return { line: subscriptionMove.line };
3848
+ if (fallback)
3849
+ [profile, projected] = [withoutSubscription(profile), withoutSubscription(projected)];
3823
3850
  const personalTurn = isPersonalBot(profile) && !subscriptionMove;
3824
3851
  // The Private Bot prefers personal engines and opted-in local compute,
3825
3852
  // then company compute. startTurn rechecks company cost on every attempt.
@@ -3839,7 +3866,7 @@ function failoverPick(turn, profile, projected, candidates, failedCandidate, res
3839
3866
  classLadder: (modelClass) => classLadder(classedModels(), modelClass),
3840
3867
  failed: attempt.selection,
3841
3868
  context: { candidates, needsDelegation: needsDelegation(tier, profile), computer, refuse: engineTierRefusal(projected), resting: engineResting,
3842
- keyBandPaused: keyBandPausedFor(turn.threadId) },
3869
+ keyBandPaused: Boolean(fallback) || keyBandPausedFor(turn.threadId) },
3843
3870
  tried,
3844
3871
  modelClass: neededClass,
3845
3872
  mayDrop: true,
@@ -3855,6 +3882,8 @@ function failoverPick(turn, profile, projected, candidates, failedCandidate, res
3855
3882
  context: { candidates, needsDelegation: needsDelegation(tier, profile), computer, refuse: engineTierRefusal(projected), resting: engineResting } })
3856
3883
  : { ...resolution, local: false };
3857
3884
  const choice = placed.choice && placed.local ? { ...placed.choice, modelClass: "Añejo" } : placed.choice;
3885
+ if (!choice && fallback)
3886
+ return { line: subscriptionMove.line };
3858
3887
  if (!choice && turn.failure?.kind === "provider_error")
3859
3888
  return { passthrough: true };
3860
3889
  if (!choice) {
@@ -3864,11 +3893,11 @@ function failoverPick(turn, profile, projected, candidates, failedCandidate, res
3864
3893
  const resting = resolution.skipped.filter((skip) => skip.reason === "resting" || skip.reason === "tried").map((skip) => skip.instanceId);
3865
3894
  const named = restingNamed([turn.instanceId, ...attempt.tried, ...resting]);
3866
3895
  return {
3867
- line: restingLine(named, restTime, cannotDriveNamed(candidates, resolution.skipped.map((skip) => skip.instanceId), computer)),
3896
+ line: restingLine(named, restTime, cannotDriveNamed(candidates, resolution.skipped.map((skip) => skip.instanceId), computer), { capped: tried.size >= MAX_ENGINES_PER_TURN }),
3868
3897
  category: named.some((engine) => engine.needsSignIn || engine.keyExpired) ? "account" : "failover_exhausted",
3869
3898
  };
3870
3899
  }
3871
- if (subscriptionMove) { /* subscriptions only: no pay-per-use episode */ }
3900
+ if (subscriptionMove || fallback) { /* subscriptions only: no pay-per-use episode */ }
3872
3901
  else if (personalMove)
3873
3902
  notePersonalPayPerUse(personalMove);
3874
3903
  else
@@ -6973,15 +7002,15 @@ function localVmSetUp() {
6973
7002
  }
6974
7003
  /** The one gate for starting the container engine (App Settings →
6975
7004
  * Computers): the person's switch, the organisation's policy, and a Local VM
6976
- * actually set up here. Returns what it started ("Colima", "Docker
6977
- * Desktop") once the engine answers, or null. */
7005
+ * actually set up here. Returns what it started (Colima, the only engine
7006
+ * Atomic starts; never Docker Desktop) once it answers, or null. */
6978
7007
  async function startLocalVmRuntime() {
6979
7008
  if (!localVmAutoStartRuntime(cfg) || !managedPolicy.computerAllowed("localVm") || !localVmSetUp())
6980
7009
  return null;
6981
7010
  try {
6982
- const { status, started } = await ensureContainerRuntime();
6983
- if (started)
6984
- console.log(`[local-vm] started ${started}: ${status.daemonUp ? "ready" : "still not answering"}`);
7011
+ const { status, started, error } = await ensureContainerRuntime();
7012
+ if (started || error)
7013
+ console[error ? "error" : "log"](`[local-vm] ${started ? `started ${started}: ${status.daemonUp ? "ready" : "still not answering"}` : "not started"}${error ? `. ${error}` : ""}`);
6985
7014
  return started && status.daemonUp ? started : null;
6986
7015
  }
6987
7016
  catch (error) {
@@ -7493,7 +7522,7 @@ async function unusableAutoPin(bot, threadId, plan, runOn) {
7493
7522
  if (pinned === "vm" && !choice.available && !choice.starting && await startLocalVmRuntime()) {
7494
7523
  choice = await computerChoice(bot, pinned);
7495
7524
  }
7496
- return choice.available || choice.starting ? null : choice.reason ?? `${surfaceLabel(pinned)} is unavailable`;
7525
+ return choice.available || choice.starting ? null : (pinned === "vm" ? containerRuntimeError() : null) ?? choice.reason ?? `${surfaceLabel(pinned)} is unavailable`;
7497
7526
  }
7498
7527
  function continueComputerSelection(threadId, generation, succeeded) {
7499
7528
  const selection = computerSelectionTurns.get(threadId);
@@ -9632,7 +9661,8 @@ async function startTurn(botId, text, opts) {
9632
9661
  : threadBusy(botId, threadId) ? undefined : turnEngineChoice(botId, threadId, { tools: true, vision: turnHasImages(opts?.userMessage) }, opts?.runOn);
9633
9662
  const tierSelection = engineChoice?.persist ? engineChoice.selection : undefined;
9634
9663
  const projectedBot = store.projectBotForTask(botId, threadId);
9635
- const bot = projectedBot && engineChoice ? { ...projectedBot, modelSelection: engineChoice.selection } : projectedBot;
9664
+ const fallbackBot = projectedBot && !opts?.engine && onSessionFallback(threadId) ? withoutSubscription(projectedBot) : projectedBot;
9665
+ const bot = fallbackBot && engineChoice ? { ...fallbackBot, modelSelection: engineChoice.selection } : fallbackBot;
9636
9666
  if (!bot)
9637
9667
  throw Object.assign(new Error("no such task"), { status: 404 });
9638
9668
  // Routines and legacy peer delivery already have their own completion
@@ -9803,6 +9833,7 @@ async function startTurn(botId, text, opts) {
9803
9833
  // thread, so this is the one moment the ledger's "who asked" may change.
9804
9834
  if (opts?.trigger)
9805
9835
  turnTriggers.set(threadId, opts.trigger);
9836
+ const expectedModel = modelExpectation(threadId);
9806
9837
  // The engine the ladder chose becomes the thread's model, as a person's
9807
9838
  // switch would; the thread stays tier-managed for its next turn.
9808
9839
  if (tierSelection && JSON.stringify(tierSelection) !== JSON.stringify(task.modelSelection)) {
@@ -9827,6 +9858,8 @@ async function startTurn(botId, text, opts) {
9827
9858
  // busy flips immediately so the composer locks; the dispatch itself runs
9828
9859
  // in the background — box provisioning can take ~90s and must never
9829
9860
  // hang the HTTP request
9861
+ const evidenceAttemptId = expectedModel ? modelEvidence.begin({ ...expectedModel, requestedModel: model,
9862
+ instanceId: instance.instanceId, provider: instance.driverKind }, Boolean(failover)) : undefined;
9830
9863
  const dispatchClaimId = randomUUID();
9831
9864
  const resourceOwner = { threadId, generation: dispatchClaimId };
9832
9865
  turnResourceOwners.set(threadId, resourceOwner);
@@ -10867,6 +10900,8 @@ async function startTurn(botId, text, opts) {
10867
10900
  }
10868
10901
  }
10869
10902
  catch (e) {
10903
+ if (evidenceAttemptId)
10904
+ modelEvidence.finishDispatch(threadId, evidenceAttemptId);
10870
10905
  handoffs.abandon(threadId, dispatchClaimId);
10871
10906
  failoverGate.untrack(threadId, dispatchClaimId);
10872
10907
  if (failoverTurns.get(threadId)?.generation === dispatchClaimId)
@@ -14311,6 +14346,8 @@ async function readyLocalVmForTurn(botId, target, isCurrent = () => true, onNote
14311
14346
  onNote?.(`Started ${started} for the Local VM`);
14312
14347
  status = await containerComputerStatus(undefined, undefined, target);
14313
14348
  }
14349
+ else if (containerRuntimeError() && isCurrent())
14350
+ onNote?.(containerRuntimeError());
14314
14351
  }
14315
14352
  noteLocalVmSeen(target, status);
14316
14353
  if (!isCurrent())
@@ -20454,14 +20491,7 @@ const handleRequest = async (req, res) => {
20454
20491
  // emoji reactions — works on any thread (1:1 or room)
20455
20492
  m = path.match(/^\/api\/threads\/([\w-]+)\/messages\/([\w-]+)\/reactions$/);
20456
20493
  if (m && method === "POST") {
20457
- const body = await readBody(req);
20458
- const emoji = String(body.emoji ?? "").slice(0, 8);
20459
- if (!emoji)
20460
- return json(res, 400, { error: "emoji required" });
20461
- const patched = store.toggleReaction(m[1], m[2], emoji, typeof body.by === "string" ? body.by : "user");
20462
- if (!patched)
20463
- return json(res, 404, { error: "no such message" });
20464
- return json(res, 200, { message: patched });
20494
+ return reactToMessage(store, req, res, m[1], m[2]);
20465
20495
  }
20466
20496
  if (path === "/api/sidebar-sections" && method === "GET") {
20467
20497
  return json(res, 200, { sections: store.sections });
@@ -20574,13 +20604,13 @@ const handleRequest = async (req, res) => {
20574
20604
  const profile = parseBotProfilePatch(body.profile, true);
20575
20605
  if (!profile.ok)
20576
20606
  return json(res, 400, { error: profile.error });
20577
- const checked = avatarGenerationRequestSchema.safeParse({ prompt: body.prompt });
20607
+ const checked = avatarGenerationRequestSchema.safeParse({ prompt: body.prompt, source: body.source });
20578
20608
  if (!checked.success)
20579
20609
  return json(res, 400, { error: checked.error.message });
20580
20610
  const { prompt } = checked.data;
20581
- const generated = await generateAvatarImage(cfg, {
20611
+ const generated = await generateRoutedAvatar(cfg, registry.instances(), {
20582
20612
  name: profile.patch.name ?? "", title: profile.patch.title ?? "", description: profile.patch.description ?? "",
20583
- }, prompt);
20613
+ }, prompt, checked.data.source);
20584
20614
  // A preview belongs to the draft. Upload it through the existing image
20585
20615
  // endpoint only when the user commits the new bot or its defaults.
20586
20616
  return json(res, 200, { dataUrl: `data:${generated.mime};base64,${generated.bytes.toString("base64")}` });
@@ -20831,7 +20861,7 @@ const handleRequest = async (req, res) => {
20831
20861
  if (!parsed.success) {
20832
20862
  return json(res, 400, { error: `prompt must be at most 400 characters` });
20833
20863
  }
20834
- const generated = await generateAvatarImage(cfg, existing, parsed.data.prompt);
20864
+ const generated = await generateRoutedAvatar(cfg, registry.instances(), existing, parsed.data.prompt, parsed.data.source);
20835
20865
  const current = store.bot(existing.id);
20836
20866
  if (!current)
20837
20867
  return json(res, 404, { error: "no such bot" });
@@ -21812,6 +21842,15 @@ const handleRequest = async (req, res) => {
21812
21842
  return json(res, 404, { error: "no such bot" });
21813
21843
  return json(res, 200, previewSystemPrompt(bot, { privateOwner: privateOwnerFor(auth) }));
21814
21844
  }
21845
+ m = path.match(/^\/api\/bots\/([\w-]+)\/model-evidence$/);
21846
+ if (m && method === "GET") {
21847
+ const thread = requestedTaskBot(m[1], url.searchParams.get("threadId") ?? undefined);
21848
+ const range = parseUsageRange(url.searchParams.get("from"), url.searchParams.get("to"));
21849
+ if (!range)
21850
+ return json(res, 400, { error: "Provide a date range of at most one year" });
21851
+ res.setHeader("cache-control", "no-store");
21852
+ return json(res, 200, modelEvidence.query(m[1], thread.threadId, range.from, range.to));
21853
+ }
21815
21854
  m = path.match(/^\/api\/bots\/([\w-]+)\/model-classes$/);
21816
21855
  if (m && method === "GET") {
21817
21856
  const thread = requestedTaskBot(m[1], url.searchParams.get("threadId") ?? undefined);