@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.
- package/dist/assets/{abnfDiagram-O67JEVCF-BkYbog6D.js → abnfDiagram-O67JEVCF-CpuRvzQ3.js} +1 -1
- package/dist/assets/{arc-QwTuMsxp.js → arc-ko1e0R6h.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-GdicO2PQ.js → architectureDiagram-NJMV4G6O-OZ3SsOOa.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-Dkr29TFF.js → blockDiagram-BEXU5L5S-DLRkBkjs.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Dz_3dstx.js → c4Diagram-YGBWAQC7-e8Q3qMnw.js} +1 -1
- package/dist/assets/channel-Lp7erhx-.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-CGsfu-PI.js → chunk-2Q5K7J3B-hCDqaM4Q.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CDqI37_6.js → chunk-3FUC2YCW-Dy3Pahu-.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-DnmSuyTb.js → chunk-5VM5RSS4-QMK3iFBF.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Dh7tXcJT.js → chunk-GTNCS2PH-6fyp9ERp.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-D_SM1ZrY.js → chunk-GWA4HPMP-DWvOhTaO.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DmeMuTsI.js → chunk-JWPE2WC7-DgohMW2S.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-R1UT9b5f.js → chunk-NETBCI7D-sRGeD2Y7.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-DpPfU1yO.js → chunk-WEXAMYUT-CZslD_VF.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DlihfTkM.js → chunk-XXDRQBXY-D7ocO3VN.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BlTV6lSC.js → classDiagram-v2-NBCMYWYE-T1lLqHHW.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-Br1kPFWw.js → cose-bilkent-JH36ORCC-BdeLiJr3.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BqPp3ZMd.js → cynefin-OW5HDTMX-BydCA0pv.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-DFfs_k8-.js → cynefinDiagram-VND7K2PF-DBxckjwQ.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-Ch3xIJA7.js → dagre-6A5THRUB-CAsvq6Zk.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DIHUD87N.js → diagram-22UHCM2B-DTwH3rA_.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CAgQQdeF.js → diagram-3UASUU5V-BNYj1bey.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-9HVKodrI.js → diagram-ATOU4E4O-CihwYFxb.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DU2kDq_h.js → diagram-CDSNMT55-nouwk3s2.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-D-qTAhHD.js → diagram-MLGK6HIB-BoKewJeA.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-C31SnI9P.js → diagram-MPIPVDR6-CrE1m9DQ.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BHrdiBVd.js → ebnfDiagram-ZINNZB2B-DRj0aU4Y.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DWyljRo5.js → elk-276RUBZZ-Do0JzCG5.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-DyY59XPp.js → erDiagram-OPXOYQCR-CRsI9pXb.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-BevMIeAy.js → flowDiagram-KWPJA3E3-dtD6wm3G.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-Ct18WHf4.js → ganttDiagram-FUAMR5RP-Cn7KyLCo.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-yhixO1N3.js → gitGraphDiagram-X574FWY7-CGH_VWzq.js} +1 -1
- package/dist/assets/index-B2fJMh8N.css +1 -0
- package/dist/assets/{index-ziyH_uer.js → index-BIcziktH.js} +253 -250
- package/dist/assets/{index-BEJ8EKQt.js → index-Bk8iAodB.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-CYa-r_S5.js → infoDiagram-VRGFBTTK-CQKsYTCL.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-CnxwM7gY.js → ishikawaDiagram-OU5B5YK6-D5catDe0.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-RKQfXBrp.js → journeyDiagram-ZHPQQLJL-KjY8mki9.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-CSlRDIF6.js → kanban-definition-PNTS6WVX-BKCyJtge.js} +1 -1
- package/dist/assets/{linear-Ch19KVyt.js → linear-B-_obUkq.js} +1 -1
- package/dist/assets/{mermaid.core-Bek7I_t6.js → mermaid.core-DlSzuDKt.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-B_LiP8su.js → mindmap-definition-NLK3R4M7-BFQSaYKh.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DhzQeiZf.js → pegDiagram-GJSIUBJH-l51izlev.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-D-BYNKHS.js → pieDiagram-5QR66LMP-6NEXRoZX.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DnCm0heP.js → quadrantDiagram-O4NWA36T-BCC9XlOe.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-COqgYnhn.js → railroadDiagram-XR7U4H2S-Dd4TmJ86.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-Dvt72ajf.js → requirementDiagram-PLB6GJNP-C1E5DaDb.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CisC9nbp.js → sankeyDiagram-IPEJSGJF-Ci6r7abb.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BO8xqvxa.js → sequenceDiagram-PO4LG4MO-BJ-I6WOh.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-DLk1jBzf.js → sizeCapture-INFHLROL-Cs5QbLOP.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-C_wutSE7.js → stateDiagram-v2-GCMORJYK-Bw--rdvq.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-hqo1UlEi.js → swimlanes-2SLR337P-B1O9IEz5.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C6wGgYKP.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-C_DJldPg.js → timeline-definition-EJHVYXUP-CTZ7DXaw.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-CsidVXMA.js → usecaseDiagram-POWQR4AR-DjwgNG0H.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-Bs-IIqn_.js → vennDiagram-UO4OBE2U-DrdJG55h.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-CXEUyxaj.js → wardleyDiagram-VNRHLVJA-HVq776nz.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-B9qvmm4P.js → xychartDiagram-PMCCYNJV-DPf9VEua.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/connector-proxy.js +205 -0
- package/dist-server/container-mcp.js +150 -96
- package/dist-server/drivers/agents-proxy.js +81 -81
- package/dist-server/index.js +7054 -6099
- package/dist-server/openmausbot.js +940 -727
- package/dist-server/pair-cli.js +940 -727
- package/dist-server/server/attachment-mimes.js +58 -0
- package/dist-server/server/attachments.js +28 -67
- package/dist-server/server/avatar-image.js +1 -0
- package/dist-server/server/avatar-subscription.js +74 -0
- package/dist-server/server/cli.js +5 -4
- package/dist-server/server/colima-docker.js +136 -0
- package/dist-server/server/composio.js +1051 -0
- package/dist-server/server/connector-proxy.js +249 -0
- package/dist-server/server/container-computer.js +31 -63
- package/dist-server/server/container-mcp.js +5 -0
- package/dist-server/server/drivers/claude-message.js +12 -0
- package/dist-server/server/drivers/claude.js +9 -12
- package/dist-server/server/drivers/codex-image.js +141 -0
- package/dist-server/server/drivers/codex.js +6 -9
- package/dist-server/server/drivers/openai-chat.js +13 -2
- package/dist-server/server/engine-failover.js +62 -12
- package/dist-server/server/engine-failure.js +2 -2
- package/dist-server/server/harness-import/history.js +5 -5
- package/dist-server/server/harness-import/test-fixtures.js +1 -1
- package/dist-server/server/index.js +75 -36
- package/dist-server/server/mcp-public-registry.js +325 -0
- package/dist-server/server/message-reactions.js +11 -0
- package/dist-server/server/model-evidence-context.js +18 -0
- package/dist-server/server/model-evidence.js +145 -0
- package/dist-server/server/port-in-use.js +21 -0
- package/dist-server/server/session-fallback.js +76 -0
- package/dist-server/server/vault/routes.js +43 -0
- package/dist-server/server/vault/runtime.js +48 -0
- package/dist-server/server/vault/store.js +177 -0
- package/dist-server/shared/connector-availability.js +12 -0
- package/dist-server/shared/desktop-private-port.js +1 -0
- package/dist-server/shared/emoji-catalog.json +1 -0
- package/dist-server/shared/emoji.js +27 -0
- package/dist-server/shared/mascot-bodies.js +9 -1
- package/dist-server/shared/model-evidence.js +25 -0
- package/dist-server/shared/subscription-bots.js +3 -0
- package/dist-server/shared/vault.js +59 -0
- package/dist-server/vps-container-mcp.js +86 -84
- package/package.json +1 -1
- package/dist/assets/channel-D2y0i2ps.js +0 -1
- package/dist/assets/index-DGWeJcck.css +0 -1
- 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
|
-
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 = (
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 (
|
|
6977
|
-
* Desktop
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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);
|