@chibitek/chibi 0.1.158 → 0.1.161
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-B8-8zzMS.js → abnfDiagram-O67JEVCF-Bb-Unz3V.js} +1 -1
- package/dist/assets/{arc-CSHQ9Dx3.js → arc-HBMH7B4K.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-BCHM9rXW.js → architectureDiagram-NJMV4G6O-CYwkb2PE.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-B-5uxPFN.js → blockDiagram-BEXU5L5S-DPYYNxBH.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-CuKNM_MR.js → c4Diagram-YGBWAQC7-DZ9hKDlA.js} +1 -1
- package/dist/assets/channel-Nx85fHZ4.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BNF02GM4.js → chunk-2Q5K7J3B-OKrUlts6.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-uEWgBd4M.js → chunk-3FUC2YCW-Tuur0lqz.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-eSZuXctY.js → chunk-5VM5RSS4-BL8QOZeP.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-CslS8nRS.js → chunk-GTNCS2PH-DMi8-ixt.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-w71-MeKj.js → chunk-GWA4HPMP-Dd7yNJ0G.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-Bjju7WGh.js → chunk-JWPE2WC7-g2dOoIMX.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-HxrroD4M.js → chunk-NETBCI7D-Cum7feOh.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-D7Rz470x.js → chunk-WEXAMYUT-BzCC1jc4.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BWKnAeyM.js → chunk-XXDRQBXY-Ck63jUMo.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CeBDRHmP.js → classDiagram-v2-NBCMYWYE-DFSuRVMZ.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BBbRKlvr.js → cose-bilkent-JH36ORCC-CegRj_kY.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-W02cnJ0B.js → cynefin-OW5HDTMX-cZS_kt1A.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Cs47eaxd.js → cynefinDiagram-VND7K2PF-DfQa9Rht.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-8mG18hzT.js → dagre-6A5THRUB-DJ9qftJu.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DF4Qrw67.js → diagram-22UHCM2B-KXUGUahJ.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-3BAVoed1.js → diagram-3UASUU5V-BudCYrCp.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-CKO3pECE.js → diagram-ATOU4E4O-Du2O03NS.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CZhrGvur.js → diagram-CDSNMT55-DXybXGYf.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-C07l9pdQ.js → diagram-MLGK6HIB-DfrknXGE.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-D3or4lkZ.js → diagram-MPIPVDR6-C29FmaJm.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DV2epXp_.js → ebnfDiagram-ZINNZB2B-BF9wS0Nz.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DlvEn4-1.js → elk-276RUBZZ-B2Fah1VF.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CB6X7Wt5.js → erDiagram-OPXOYQCR-Di3q4bpF.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-C3bqz4lC.js → flowDiagram-KWPJA3E3-D1UiqwbF.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-yo_Evhab.js → ganttDiagram-FUAMR5RP-BjZqMt7o.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Cq2oUlap.js → gitGraphDiagram-X574FWY7-DiZtdip7.js} +1 -1
- package/dist/assets/index-B2fJMh8N.css +1 -0
- package/dist/assets/{index-CMcr5k3Z.js → index-XFwW09vd.js} +246 -246
- package/dist/assets/{index-CG4AFPb4.js → index-dbQveHan.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-Y3ZrY7qL.js → infoDiagram-VRGFBTTK-BJUJCgio.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BpOtSfyi.js → ishikawaDiagram-OU5B5YK6-BX0XdHUx.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-UQM727le.js → journeyDiagram-ZHPQQLJL-Bg904IIx.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-C3s5oHpY.js → kanban-definition-PNTS6WVX-hkZMFMHZ.js} +1 -1
- package/dist/assets/{linear-hX6nFOQr.js → linear-Bh29RTbQ.js} +1 -1
- package/dist/assets/{mermaid.core-DZObGLN7.js → mermaid.core-aqB1jW_w.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Br-i2E3P.js → mindmap-definition-NLK3R4M7-D5GMV-Zd.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-D5SERB-N.js → pegDiagram-GJSIUBJH-7_wFtkFH.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-BPNf4J3D.js → pieDiagram-5QR66LMP-DXtZBQ4f.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Bm_Wh7Jr.js → quadrantDiagram-O4NWA36T-C1v3ydTF.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-D6bo0abD.js → railroadDiagram-XR7U4H2S-CLmhiOXg.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP--1n5rkV7.js → requirementDiagram-PLB6GJNP-BKxI2rh_.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-Ct4tP2kK.js → sankeyDiagram-IPEJSGJF-gKK-Qs7b.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CCG7O5fR.js → sequenceDiagram-PO4LG4MO-BT94Uowp.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CJJJra9c.js → sizeCapture-INFHLROL-RDJL6sCM.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BMdRvVEX.js → stateDiagram-v2-GCMORJYK-DqHMzv-J.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-B1wnogip.js → swimlanes-2SLR337P-B-NN0ql6.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BO2gR34z.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-l-3yhXUP.js → timeline-definition-EJHVYXUP-C51S9tpN.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-DtwtCOoJ.js → usecaseDiagram-POWQR4AR-DyfNMmhq.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-BZAEce1B.js → vennDiagram-UO4OBE2U-D924r33S.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-Na_BCMMO.js → wardleyDiagram-VNRHLVJA-DXSfsjUr.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BW9dlUfE.js → xychartDiagram-PMCCYNJV-mYX9tnBU.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +1 -1
- package/dist-server/index.js +3611 -3082
- package/dist-server/openmausbot.js +370 -162
- package/dist-server/pair-cli.js +370 -162
- 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 +24 -1
- package/dist-server/server/checkpoint-recovery.js +18 -0
- package/dist-server/server/drivers/acp/core.js +17 -17
- 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 +44 -4
- 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 +40 -26
- 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/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/vps-container-mcp.js +1 -1
- package/package.json +1 -1
- package/dist/assets/channel-bPN03jIt.js +0 -1
- package/dist/assets/index-CZjRcUdn.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-DWyjdGGR.js +0 -8
- package/dist-server/connector-proxy.js +0 -205
- package/dist-server/server/composio.js +0 -1051
- package/dist-server/server/connector-proxy.js +0 -249
- package/dist-server/server/mcp-public-registry.js +0 -325
- package/dist-server/shared/connector-availability.js +0 -12
|
@@ -1,3 +1,37 @@
|
|
|
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
36
|
/** The two-try cap: at most this many engines take part in one turn, the
|
|
3
37
|
* first and one switch. Every failed attempt counts toward it, whatever the
|
|
@@ -15,10 +49,10 @@ export const NO_ANSWER_FAILURE = "The engine ended the turn without an answer.";
|
|
|
15
49
|
function unrecognisedFailure(turn) {
|
|
16
50
|
return { kind: "provider_error", message: turn.lastError ?? NO_ANSWER_FAILURE };
|
|
17
51
|
}
|
|
18
|
-
/**
|
|
19
|
-
*
|
|
20
|
-
* turn
|
|
21
|
-
const NOT_A_PROVIDER_FAILURE = new Set(["interrupted", "cancelled"]);
|
|
52
|
+
/** Stops that must not dispatch the request again: cancellation or a broken
|
|
53
|
+
* checkpoint whose accepted prompt may already have executed actions.
|
|
54
|
+
* The next explicit turn rebuilds that checkpoint from retained history. */
|
|
55
|
+
const NOT_A_PROVIDER_FAILURE = new Set(["interrupted", "cancelled", "invalid_checkpoint"]);
|
|
22
56
|
export class FailoverGate {
|
|
23
57
|
turns = new Map();
|
|
24
58
|
onFailure;
|
|
@@ -224,3 +258,9 @@ export function restingLine(engines, formatTime, computer, options = {}) {
|
|
|
224
258
|
return `${parts[0]}, and no other engine at the same level is free, so this turn stopped.${hint}`;
|
|
225
259
|
return `Every engine this bot can use is resting, so this turn stopped: ${parts.join(", ")}.${hint}`;
|
|
226
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,6 @@
|
|
|
1
|
+
import { invalidateBrokenCheckpoint } from "./checkpoint-recovery.js";
|
|
2
|
+
import { createModelExpectation, ModelEvidenceLedger } from "./model-evidence-context.js";
|
|
3
|
+
import { reactToMessage } from "./message-reactions.js";
|
|
1
4
|
import { hasUnboundGoalGuard, goalGuardForEvent, addGoalGuard, removeGoalGuard, runPersonalGoalTurn, updatePersonalGoalReceipt } from "./personal-goal-turn.js";
|
|
2
5
|
import { PersonalGoalRuntime, receiveGoalCommand } from "./personal-goal-runtime.js";
|
|
3
6
|
import { personalGoalInstructions } from "./personal-goals.js";
|
|
@@ -46,7 +49,8 @@ import { validateBotCwd } from "./bot-cwd.js";
|
|
|
46
49
|
import { ATTACHMENTS_DIR, attachmentExists, cleanupStaleAttachmentPartials, deleteAttachment, extensionForMime, FILE_MAX_BYTES, IMAGE_MAX_BYTES, readAttachment, saveFile, saveImage, saveImageUpload, validateAttachmentUploadId, } from "./attachments.js";
|
|
47
50
|
import { companyCheck, companyContext } from "./company-document-check.js";
|
|
48
51
|
import { messageFileDisposition, messageFileDownloadName, messageAttachmentName, messageFileRoots, messageImageTargetAt, messageReferencesFile, openMessageFile, } from "./message-file.js";
|
|
49
|
-
import { avatarGenerationRequestSchema, avatarGenerationStateMatches,
|
|
52
|
+
import { avatarGenerationRequestSchema, avatarGenerationStateMatches, avatarImageStatus, snapshotAvatarGenerationState } from "./avatar-image.js";
|
|
53
|
+
import { generateRoutedAvatar, installAvatarSubscriptionRoute } from "./avatar-subscription.js";
|
|
50
54
|
import { fitsOnOneLine, parseBotProfilePatch } from "./bot-profile.js";
|
|
51
55
|
import { groupTurnCwd } from "./room-cwd.js";
|
|
52
56
|
import { RepoRegistry } from "./repos.js";
|
|
@@ -304,7 +308,7 @@ import { createNexusMemoryRoutes } from "./routes/nexus-memory.js";
|
|
|
304
308
|
import { createBoardRoutes } from "./routes/board.js";
|
|
305
309
|
import { readBoardFromNexus } from "./board/nexus-board.js";
|
|
306
310
|
import { LocalModelInventory, localModePlacement, machineMemory } from "./local-models-mode.js";
|
|
307
|
-
import { FailoverGate, MAX_ENGINES_PER_TURN, continuationNote, restingLine } from "./engine-failover.js";
|
|
311
|
+
import { eventBase, restTime, FailoverGate, MAX_ENGINES_PER_TURN, continuationNote, restingLine } from "./engine-failover.js";
|
|
308
312
|
import { enterSessionFallback, onSessionFallback, ownOrFallback, sessionFallbackSpentLine, withoutSubscription } from "./session-fallback.js";
|
|
309
313
|
import { TURN_ERROR_CATEGORIES } from "../shared/runtime-events.js";
|
|
310
314
|
import { EngineProbes, PROBE_PROMPT, isProbeThread, probeThreadId } from "./engine-probe.js";
|
|
@@ -487,6 +491,7 @@ function customDomainStatus() {
|
|
|
487
491
|
};
|
|
488
492
|
}
|
|
489
493
|
const registry = new ProviderRegistry(BUILT_IN_DRIVERS);
|
|
494
|
+
installAvatarSubscriptionRoute(() => registry.instances());
|
|
490
495
|
// Engines installed from Settings live under the data directory and win over
|
|
491
496
|
// any other copy on PATH.
|
|
492
497
|
registerEnginesBinDir();
|
|
@@ -3564,6 +3569,9 @@ const roomFailoverTurns = new Map();
|
|
|
3564
3569
|
* default (a resting engine's stand-in, a failover): the ledger books it. */
|
|
3565
3570
|
const roomTurnSelections = new Map();
|
|
3566
3571
|
const turnEngineOverrides = new Map();
|
|
3572
|
+
const modelEvidence = new ModelEvidenceLedger(DATA_DIR);
|
|
3573
|
+
const modelExpectation = createModelExpectation({ get store() { return store; }, get registry() { return registry; }, turnEngineOverrides, roomTurnSelections,
|
|
3574
|
+
speakerBot: threadId => groupSpeakers.get(threadId)?.botId, enginePlan, botClassChoices });
|
|
3567
3575
|
const failoverGate = new FailoverGate((turn, completion) => engineFailover(turn, completion));
|
|
3568
3576
|
/** An approval shows a shell command as the model meant it: the RTK add-on's
|
|
3569
3577
|
* launcher, which engines that take RTK as instructions put in front of
|
|
@@ -3582,9 +3590,16 @@ bus.setGate((published) => {
|
|
|
3582
3590
|
return [];
|
|
3583
3591
|
}
|
|
3584
3592
|
const event = withoutRtkInSummary(published);
|
|
3593
|
+
if (!shouldIgnoreProviderEvent(event))
|
|
3594
|
+
modelEvidence.observe(event, () => modelExpectation(event.threadId, event.providerInstanceId, event.provider));
|
|
3585
3595
|
// [jev-verifier] note a failover turn that finished, before the gate forgets it.
|
|
3586
3596
|
const jevCandidate = jevAnswerCheckCandidate(event);
|
|
3587
3597
|
const delivered = failoverGate.filter(event);
|
|
3598
|
+
// Gate-generated switches never re-enter publish; preserve their evidence too.
|
|
3599
|
+
for (const out of delivered)
|
|
3600
|
+
if (out.type === "turn.engine_failover" && out !== event) {
|
|
3601
|
+
modelEvidence.observe(out, () => modelExpectation(out.threadId, out.providerInstanceId, out.provider));
|
|
3602
|
+
}
|
|
3588
3603
|
if (jevCandidate)
|
|
3589
3604
|
setImmediate(() => void jevAnswerCheck(jevCandidate));
|
|
3590
3605
|
for (const out of delivered)
|
|
@@ -3603,12 +3618,6 @@ function noteEngineBack(event) {
|
|
|
3603
3618
|
const model = selection?.instanceId === instanceId ? selection.model : undefined;
|
|
3604
3619
|
engineRests.noteBack(instanceId, candidate ? engineAccount(candidate) : undefined, candidate ? restScope(candidate, model) : undefined);
|
|
3605
3620
|
}
|
|
3606
|
-
/** A new harness event about the same provider turn as `event`. */
|
|
3607
|
-
const eventBase = (event) => ({
|
|
3608
|
-
eventId: newEventId(), provider: event.provider, threadId: event.threadId, createdAt: new Date().toISOString(),
|
|
3609
|
-
...(event.providerInstanceId ? { providerInstanceId: event.providerInstanceId } : {}), ...(event.turnId ? { turnId: event.turnId } : {}),
|
|
3610
|
-
});
|
|
3611
|
-
const restTime = (at) => new Date(at).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
3612
3621
|
/** Rest a refused engine and continue on an eligible engine, or show an actionable line. */
|
|
3613
3622
|
function engineFailover(turn, completion) {
|
|
3614
3623
|
const failure = turn.failure;
|
|
@@ -8162,22 +8171,20 @@ bus.subscribe((event) => {
|
|
|
8162
8171
|
});
|
|
8163
8172
|
break;
|
|
8164
8173
|
case "runtime.error":
|
|
8174
|
+
if (bot)
|
|
8175
|
+
invalidateBrokenCheckpoint(store, bot.id, event.providerInstanceId ? registry.get(event.providerInstanceId)?.driverKind : undefined, event);
|
|
8165
8176
|
if (event.message?.trim())
|
|
8166
8177
|
lastTurnError.set(event.threadId, event.message.trim().slice(0, 300));
|
|
8167
8178
|
pushMessage({
|
|
8168
8179
|
role: "bot",
|
|
8169
8180
|
kind: "activity",
|
|
8170
|
-
//
|
|
8171
|
-
// same way; an error line is never a lost connection either way.
|
|
8181
|
+
// Keep the category consistent across surfaces.
|
|
8172
8182
|
// The harness's own stop line is kept whole: it ends with where to fix
|
|
8173
8183
|
// the account, which a 160-character cut would drop.
|
|
8174
8184
|
tool: { name: `error: ${event.message.slice(0, event.category === "account" || event.category === "failover_exhausted" ? 600 : 160)}`, ok: false, setup: event.setup, ...(event.terminal ? { terminal: true } : {}),
|
|
8175
8185
|
...(event.category ? { errorCategory: event.category } : {}) },
|
|
8176
8186
|
});
|
|
8177
|
-
//
|
|
8178
|
-
// dead until something changes, not merely idle. The next successful
|
|
8179
|
-
// dispatch moves it to working; turn.completed (which follows a setup
|
|
8180
|
-
// failure) is told to leave "dead" alone.
|
|
8187
|
+
// Setup failures remain dead until a later successful dispatch.
|
|
8181
8188
|
if (event.setup && bot)
|
|
8182
8189
|
store.setTaskActivity(bot.id, event.threadId, "dead");
|
|
8183
8190
|
break;
|
|
@@ -9825,6 +9832,7 @@ async function startTurn(botId, text, opts) {
|
|
|
9825
9832
|
// thread, so this is the one moment the ledger's "who asked" may change.
|
|
9826
9833
|
if (opts?.trigger)
|
|
9827
9834
|
turnTriggers.set(threadId, opts.trigger);
|
|
9835
|
+
const expectedModel = modelExpectation(threadId);
|
|
9828
9836
|
// The engine the ladder chose becomes the thread's model, as a person's
|
|
9829
9837
|
// switch would; the thread stays tier-managed for its next turn.
|
|
9830
9838
|
if (tierSelection && JSON.stringify(tierSelection) !== JSON.stringify(task.modelSelection)) {
|
|
@@ -9849,6 +9857,8 @@ async function startTurn(botId, text, opts) {
|
|
|
9849
9857
|
// busy flips immediately so the composer locks; the dispatch itself runs
|
|
9850
9858
|
// in the background — box provisioning can take ~90s and must never
|
|
9851
9859
|
// hang the HTTP request
|
|
9860
|
+
const evidenceAttemptId = expectedModel ? modelEvidence.begin({ ...expectedModel, requestedModel: model,
|
|
9861
|
+
instanceId: instance.instanceId, provider: instance.driverKind }, Boolean(failover)) : undefined;
|
|
9852
9862
|
const dispatchClaimId = randomUUID();
|
|
9853
9863
|
const resourceOwner = { threadId, generation: dispatchClaimId };
|
|
9854
9864
|
turnResourceOwners.set(threadId, resourceOwner);
|
|
@@ -10889,6 +10899,8 @@ async function startTurn(botId, text, opts) {
|
|
|
10889
10899
|
}
|
|
10890
10900
|
}
|
|
10891
10901
|
catch (e) {
|
|
10902
|
+
if (evidenceAttemptId)
|
|
10903
|
+
modelEvidence.finishDispatch(threadId, evidenceAttemptId);
|
|
10892
10904
|
handoffs.abandon(threadId, dispatchClaimId);
|
|
10893
10905
|
failoverGate.untrack(threadId, dispatchClaimId);
|
|
10894
10906
|
if (failoverTurns.get(threadId)?.generation === dispatchClaimId)
|
|
@@ -20478,14 +20490,7 @@ const handleRequest = async (req, res) => {
|
|
|
20478
20490
|
// emoji reactions — works on any thread (1:1 or room)
|
|
20479
20491
|
m = path.match(/^\/api\/threads\/([\w-]+)\/messages\/([\w-]+)\/reactions$/);
|
|
20480
20492
|
if (m && method === "POST") {
|
|
20481
|
-
|
|
20482
|
-
const emoji = String(body.emoji ?? "").slice(0, 8);
|
|
20483
|
-
if (!emoji)
|
|
20484
|
-
return json(res, 400, { error: "emoji required" });
|
|
20485
|
-
const patched = store.toggleReaction(m[1], m[2], emoji, typeof body.by === "string" ? body.by : "user");
|
|
20486
|
-
if (!patched)
|
|
20487
|
-
return json(res, 404, { error: "no such message" });
|
|
20488
|
-
return json(res, 200, { message: patched });
|
|
20493
|
+
return reactToMessage(store, req, res, m[1], m[2]);
|
|
20489
20494
|
}
|
|
20490
20495
|
if (path === "/api/sidebar-sections" && method === "GET") {
|
|
20491
20496
|
return json(res, 200, { sections: store.sections });
|
|
@@ -20598,13 +20603,13 @@ const handleRequest = async (req, res) => {
|
|
|
20598
20603
|
const profile = parseBotProfilePatch(body.profile, true);
|
|
20599
20604
|
if (!profile.ok)
|
|
20600
20605
|
return json(res, 400, { error: profile.error });
|
|
20601
|
-
const checked = avatarGenerationRequestSchema.safeParse({ prompt: body.prompt });
|
|
20606
|
+
const checked = avatarGenerationRequestSchema.safeParse({ prompt: body.prompt, source: body.source });
|
|
20602
20607
|
if (!checked.success)
|
|
20603
20608
|
return json(res, 400, { error: checked.error.message });
|
|
20604
20609
|
const { prompt } = checked.data;
|
|
20605
|
-
const generated = await
|
|
20610
|
+
const generated = await generateRoutedAvatar(cfg, registry.instances(), {
|
|
20606
20611
|
name: profile.patch.name ?? "", title: profile.patch.title ?? "", description: profile.patch.description ?? "",
|
|
20607
|
-
}, prompt);
|
|
20612
|
+
}, prompt, checked.data.source);
|
|
20608
20613
|
// A preview belongs to the draft. Upload it through the existing image
|
|
20609
20614
|
// endpoint only when the user commits the new bot or its defaults.
|
|
20610
20615
|
return json(res, 200, { dataUrl: `data:${generated.mime};base64,${generated.bytes.toString("base64")}` });
|
|
@@ -20855,7 +20860,7 @@ const handleRequest = async (req, res) => {
|
|
|
20855
20860
|
if (!parsed.success) {
|
|
20856
20861
|
return json(res, 400, { error: `prompt must be at most 400 characters` });
|
|
20857
20862
|
}
|
|
20858
|
-
const generated = await
|
|
20863
|
+
const generated = await generateRoutedAvatar(cfg, registry.instances(), existing, parsed.data.prompt, parsed.data.source);
|
|
20859
20864
|
const current = store.bot(existing.id);
|
|
20860
20865
|
if (!current)
|
|
20861
20866
|
return json(res, 404, { error: "no such bot" });
|
|
@@ -21836,6 +21841,15 @@ const handleRequest = async (req, res) => {
|
|
|
21836
21841
|
return json(res, 404, { error: "no such bot" });
|
|
21837
21842
|
return json(res, 200, previewSystemPrompt(bot, { privateOwner: privateOwnerFor(auth) }));
|
|
21838
21843
|
}
|
|
21844
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/model-evidence$/);
|
|
21845
|
+
if (m && method === "GET") {
|
|
21846
|
+
const thread = requestedTaskBot(m[1], url.searchParams.get("threadId") ?? undefined);
|
|
21847
|
+
const range = parseUsageRange(url.searchParams.get("from"), url.searchParams.get("to"));
|
|
21848
|
+
if (!range)
|
|
21849
|
+
return json(res, 400, { error: "Provide a date range of at most one year" });
|
|
21850
|
+
res.setHeader("cache-control", "no-store");
|
|
21851
|
+
return json(res, 200, modelEvidence.query(m[1], thread.threadId, range.from, range.to));
|
|
21852
|
+
}
|
|
21839
21853
|
m = path.match(/^\/api\/bots\/([\w-]+)\/model-classes$/);
|
|
21840
21854
|
if (m && method === "GET") {
|
|
21841
21855
|
const thread = requestedTaskBot(m[1], url.searchParams.get("threadId") ?? undefined);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { isReactionEmoji } from "../shared/emoji.js";
|
|
2
|
+
import { json, readBody } from "./harness/http.js";
|
|
3
|
+
export async function reactToMessage(store, req, res, threadId, messageId) {
|
|
4
|
+
const body = await readBody(req);
|
|
5
|
+
if (!isReactionEmoji(body.emoji))
|
|
6
|
+
return json(res, 400, { error: "Choose one emoji" });
|
|
7
|
+
const message = store.toggleReaction(threadId, messageId, body.emoji, "user");
|
|
8
|
+
if (!message)
|
|
9
|
+
return json(res, 404, { error: "no such message" });
|
|
10
|
+
return json(res, 200, { message });
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { ModelEvidenceLedger } from "./model-evidence.js";
|
|
2
|
+
// Snapshot the same class choices the model picker displays before routing mutates the task.
|
|
3
|
+
import { threadEngineMode } from "../shared/engine-tiers.js";
|
|
4
|
+
export function createModelExpectation(context) {
|
|
5
|
+
return (threadId, instanceId, provider) => {
|
|
6
|
+
const profile = context.store.botByThread(threadId) ?? context.store.bot(context.speakerBot(threadId) ?? "");
|
|
7
|
+
if (!profile)
|
|
8
|
+
return null;
|
|
9
|
+
const task = context.store.taskByThread(profile.id, threadId);
|
|
10
|
+
const selection = context.turnEngineOverrides.get(threadId) ?? context.roomTurnSelections.get(threadId) ?? task?.modelSelection ?? profile.modelSelection;
|
|
11
|
+
const modelClass = context.enginePlan(profile, threadId).modelClass;
|
|
12
|
+
const mode = threadEngineMode(profile, task ?? {});
|
|
13
|
+
const preview = mode.kind === "pinned" ? undefined : context.botClassChoices(profile, threadId).options.find(option => option.modelClass === modelClass);
|
|
14
|
+
return { botId: profile.id, threadId, label: `${mode.kind === "tier" ? "Automatic: " : ""}${modelClass} · ${preview?.label ?? selection.model}`, modelClass,
|
|
15
|
+
model: preview?.selection?.model ?? selection.model, requestedModel: selection.model,
|
|
16
|
+
instanceId: instanceId ?? selection.instanceId, provider: provider ?? context.registry.get(selection.instanceId)?.driverKind ?? "unknown" };
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Name: model-evidence; author: Atomic Engineering; date: 2026-10-27.
|
|
2
|
+
// Append-only identity history beside the existing usage ledger, without prompts.
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { mkdirSync, appendFileSync, readdirSync, readFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { modelEvidenceMetrics } from "../shared/model-evidence.js";
|
|
7
|
+
export { modelEvidenceMetrics };
|
|
8
|
+
const safe = (value) => typeof value === "string" && /^[\w .:/@+()[\]-]{1,200}$/u.test(value) ? value : undefined;
|
|
9
|
+
export class ModelEvidenceLedger {
|
|
10
|
+
active = new Map();
|
|
11
|
+
byTurn = new Map();
|
|
12
|
+
failures = 0;
|
|
13
|
+
dataDir;
|
|
14
|
+
constructor(dataDir) { this.dataDir = dataDir; }
|
|
15
|
+
begin(expected, continuation = false) {
|
|
16
|
+
const prior = this.active.get(expected.threadId);
|
|
17
|
+
const attempt = { expected: { ...expected, ...(continuation && prior ? { label: prior.expected.label, modelClass: prior.expected.modelClass, model: prior.expected.model } : {}) }, logicalTurnId: continuation && prior ? prior.logicalTurnId : randomUUID(), attemptId: randomUUID() };
|
|
18
|
+
this.active.set(expected.threadId, attempt);
|
|
19
|
+
this.write(attempt, { kind: "dispatch", level: expected.requestedModel || expected.model ? "requested-only" : "unknown" });
|
|
20
|
+
return attempt.attemptId;
|
|
21
|
+
}
|
|
22
|
+
finishDispatch(threadId, attemptId) {
|
|
23
|
+
const attempt = this.active.get(threadId);
|
|
24
|
+
if (attempt?.attemptId === attemptId)
|
|
25
|
+
this.write(attempt, { kind: "completed", level: "unknown", ok: false, reason: "dispatch ended before provider completion" });
|
|
26
|
+
}
|
|
27
|
+
write(attempt, fields) {
|
|
28
|
+
const row = { ...attempt.expected, version: 1, id: randomUUID(), at: new Date().toISOString(),
|
|
29
|
+
logicalTurnId: attempt.logicalTurnId, attemptId: attempt.attemptId, ...(attempt.turnId ? { turnId: attempt.turnId } : {}), ...fields };
|
|
30
|
+
try {
|
|
31
|
+
const directory = join(this.dataDir, "model-evidence");
|
|
32
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
33
|
+
appendFileSync(join(directory, `${row.at.slice(0, 7)}.jsonl`), JSON.stringify(row) + "\n", { mode: 0o600 });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
this.failures++;
|
|
37
|
+
if (this.failures === 1)
|
|
38
|
+
console.error("[model-evidence] Cannot persist model history; queries will report incomplete evidence.");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
observe(event, expectation) {
|
|
42
|
+
if (!["turn.started", "session.started", "model.evidence", "model.call", "turn.retrying", "turn.engine_failover", "session.model-fallback", "turn.completed", "session.exited"].includes(event.type))
|
|
43
|
+
return;
|
|
44
|
+
const turnKey = event.turnId ? JSON.stringify([event.threadId, event.providerInstanceId ?? event.provider, event.turnId]) : undefined;
|
|
45
|
+
let attempt = turnKey ? this.byTurn.get(turnKey) : undefined;
|
|
46
|
+
if (!attempt) {
|
|
47
|
+
attempt = this.active.get(event.threadId);
|
|
48
|
+
if (!attempt || (event.type === "turn.started" && attempt.turnId && attempt.turnId !== event.turnId)) {
|
|
49
|
+
const expected = expectation();
|
|
50
|
+
if (!expected)
|
|
51
|
+
return;
|
|
52
|
+
this.begin(expected);
|
|
53
|
+
attempt = this.active.get(event.threadId);
|
|
54
|
+
}
|
|
55
|
+
if (event.turnId) {
|
|
56
|
+
attempt.turnId = event.turnId;
|
|
57
|
+
this.byTurn.set(turnKey, attempt);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const base = { provider: event.provider, instanceId: event.providerInstanceId ?? attempt.expected.instanceId };
|
|
61
|
+
switch (event.type) {
|
|
62
|
+
case "turn.started":
|
|
63
|
+
this.write(attempt, { ...base, kind: "start", level: "requested-only" });
|
|
64
|
+
break;
|
|
65
|
+
case "model.call":
|
|
66
|
+
this.write(attempt, { ...base, kind: "call", level: "unknown", callId: event.callId });
|
|
67
|
+
break;
|
|
68
|
+
case "model.evidence":
|
|
69
|
+
this.write(attempt, { ...base, kind: "evidence", level: safe(event.model) ? event.level : "unknown",
|
|
70
|
+
...(safe(event.model) ? { reportedModel: safe(event.model) } : {}), callId: event.callId, responseId: safe(event.responseId) });
|
|
71
|
+
break;
|
|
72
|
+
case "session.started":
|
|
73
|
+
this.write(attempt, { ...base, kind: "evidence", level: safe(event.model) ? event.modelEvidenceLevel ?? "requested-only" : "unknown", reportedModel: safe(event.model) });
|
|
74
|
+
break;
|
|
75
|
+
case "turn.retrying": {
|
|
76
|
+
this.write(attempt, { ...base, kind: "retry", level: "unknown", reason: safe(event.reason) });
|
|
77
|
+
// CLI retries reuse their native turn id, but are distinct attempts.
|
|
78
|
+
this.begin(attempt.expected, true);
|
|
79
|
+
const retry = this.active.get(event.threadId);
|
|
80
|
+
retry.turnId = event.turnId;
|
|
81
|
+
if (turnKey)
|
|
82
|
+
this.byTurn.set(turnKey, retry);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case "turn.engine_failover":
|
|
86
|
+
case "session.model-fallback":
|
|
87
|
+
this.write(attempt, { ...base, kind: "fallback", level: "unknown", reason: safe(event.reason) ?? "provider fallback" });
|
|
88
|
+
break;
|
|
89
|
+
case "turn.completed":
|
|
90
|
+
this.write(attempt, { ...base, kind: "completed", level: "unknown", ok: event.ok });
|
|
91
|
+
break;
|
|
92
|
+
case "session.exited":
|
|
93
|
+
this.write(attempt, { ...base, kind: "exited", level: "unknown" });
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
// ponytail: bounded correlation cache; durable history remains on disk.
|
|
97
|
+
if (this.byTurn.size > 10000)
|
|
98
|
+
this.byTurn.delete(this.byTurn.keys().next().value);
|
|
99
|
+
if (this.active.size > 10000)
|
|
100
|
+
this.active.delete(this.active.keys().next().value);
|
|
101
|
+
}
|
|
102
|
+
query(botId, threadId, from, to) {
|
|
103
|
+
const directory = join(this.dataDir, "model-evidence");
|
|
104
|
+
const rows = [];
|
|
105
|
+
let incomplete = this.failures > 0;
|
|
106
|
+
let files = [];
|
|
107
|
+
try {
|
|
108
|
+
files = readdirSync(directory).filter(file => /^\d{4}-\d{2}\.jsonl$/.test(file)).sort();
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (error.code !== "ENOENT")
|
|
112
|
+
incomplete = true;
|
|
113
|
+
}
|
|
114
|
+
for (const file of files) {
|
|
115
|
+
if (file.slice(0, 7) < from.toISOString().slice(0, 7) || file.slice(0, 7) > to.toISOString().slice(0, 7))
|
|
116
|
+
continue;
|
|
117
|
+
try {
|
|
118
|
+
for (const line of readFileSync(join(directory, file), "utf8").split("\n").filter(Boolean)) {
|
|
119
|
+
try {
|
|
120
|
+
const row = JSON.parse(line);
|
|
121
|
+
if (row.version !== 1 || !row.logicalTurnId || !row.attemptId) {
|
|
122
|
+
incomplete = true;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (row.botId === botId && row.threadId === threadId && row.at >= from.toISOString() && row.at <= to.toISOString())
|
|
126
|
+
rows.push(row);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
incomplete = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
incomplete = true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const latestDispatch = rows.findLast(row => row.kind === "dispatch");
|
|
138
|
+
const latest = latestDispatch && rows.findLast(row => row.attemptId === latestDispatch.attemptId);
|
|
139
|
+
// Late exit/events from an older attempt must not settle a newer attempt.
|
|
140
|
+
const running = latest && !rows.some(row => row.attemptId === latest.attemptId && ["completed", "exited"].includes(row.kind));
|
|
141
|
+
return { rows, metrics: modelEvidenceMetrics(rows), incomplete, writeFailures: this.failures,
|
|
142
|
+
current: running && this.active.has(threadId) ? latest.logicalTurnId : null,
|
|
143
|
+
interrupted: Boolean(running && !this.active.has(threadId)), last: latest?.logicalTurnId ?? null };
|
|
144
|
+
}
|
|
145
|
+
}
|