@chibitek/chibi 0.1.158 → 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 (89) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-B8-8zzMS.js → abnfDiagram-O67JEVCF-CpuRvzQ3.js} +1 -1
  2. package/dist/assets/{arc-CSHQ9Dx3.js → arc-ko1e0R6h.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-BCHM9rXW.js → architectureDiagram-NJMV4G6O-OZ3SsOOa.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-B-5uxPFN.js → blockDiagram-BEXU5L5S-DLRkBkjs.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-CuKNM_MR.js → c4Diagram-YGBWAQC7-e8Q3qMnw.js} +1 -1
  6. package/dist/assets/channel-Lp7erhx-.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-BNF02GM4.js → chunk-2Q5K7J3B-hCDqaM4Q.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-uEWgBd4M.js → chunk-3FUC2YCW-Dy3Pahu-.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-eSZuXctY.js → chunk-5VM5RSS4-QMK3iFBF.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-CslS8nRS.js → chunk-GTNCS2PH-6fyp9ERp.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-w71-MeKj.js → chunk-GWA4HPMP-DWvOhTaO.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-Bjju7WGh.js → chunk-JWPE2WC7-DgohMW2S.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-HxrroD4M.js → chunk-NETBCI7D-sRGeD2Y7.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-D7Rz470x.js → chunk-WEXAMYUT-CZslD_VF.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-BWKnAeyM.js → chunk-XXDRQBXY-D7ocO3VN.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-CeBDRHmP.js → classDiagram-v2-NBCMYWYE-T1lLqHHW.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-BBbRKlvr.js → cose-bilkent-JH36ORCC-BdeLiJr3.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-W02cnJ0B.js → cynefin-OW5HDTMX-BydCA0pv.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-Cs47eaxd.js → cynefinDiagram-VND7K2PF-DBxckjwQ.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-8mG18hzT.js → dagre-6A5THRUB-CAsvq6Zk.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-DF4Qrw67.js → diagram-22UHCM2B-DTwH3rA_.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-3BAVoed1.js → diagram-3UASUU5V-BNYj1bey.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-CKO3pECE.js → diagram-ATOU4E4O-CihwYFxb.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-CZhrGvur.js → diagram-CDSNMT55-nouwk3s2.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-C07l9pdQ.js → diagram-MLGK6HIB-BoKewJeA.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-D3or4lkZ.js → diagram-MPIPVDR6-CrE1m9DQ.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-DV2epXp_.js → ebnfDiagram-ZINNZB2B-DRj0aU4Y.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-DlvEn4-1.js → elk-276RUBZZ-Do0JzCG5.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-CB6X7Wt5.js → erDiagram-OPXOYQCR-CRsI9pXb.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-C3bqz4lC.js → flowDiagram-KWPJA3E3-dtD6wm3G.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-yo_Evhab.js → ganttDiagram-FUAMR5RP-Cn7KyLCo.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-Cq2oUlap.js → gitGraphDiagram-X574FWY7-CGH_VWzq.js} +1 -1
  33. package/dist/assets/index-B2fJMh8N.css +1 -0
  34. package/dist/assets/{index-CMcr5k3Z.js → index-BIcziktH.js} +246 -246
  35. package/dist/assets/{index-CG4AFPb4.js → index-Bk8iAodB.js} +1 -1
  36. package/dist/assets/{infoDiagram-VRGFBTTK-Y3ZrY7qL.js → infoDiagram-VRGFBTTK-CQKsYTCL.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BpOtSfyi.js → ishikawaDiagram-OU5B5YK6-D5catDe0.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-UQM727le.js → journeyDiagram-ZHPQQLJL-KjY8mki9.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-C3s5oHpY.js → kanban-definition-PNTS6WVX-BKCyJtge.js} +1 -1
  40. package/dist/assets/{linear-hX6nFOQr.js → linear-B-_obUkq.js} +1 -1
  41. package/dist/assets/{mermaid.core-DZObGLN7.js → mermaid.core-DlSzuDKt.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-Br-i2E3P.js → mindmap-definition-NLK3R4M7-BFQSaYKh.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-D5SERB-N.js → pegDiagram-GJSIUBJH-l51izlev.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-BPNf4J3D.js → pieDiagram-5QR66LMP-6NEXRoZX.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-Bm_Wh7Jr.js → quadrantDiagram-O4NWA36T-BCC9XlOe.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-D6bo0abD.js → railroadDiagram-XR7U4H2S-Dd4TmJ86.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP--1n5rkV7.js → requirementDiagram-PLB6GJNP-C1E5DaDb.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-Ct4tP2kK.js → sankeyDiagram-IPEJSGJF-Ci6r7abb.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-CCG7O5fR.js → sequenceDiagram-PO4LG4MO-BJ-I6WOh.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-CJJJra9c.js → sizeCapture-INFHLROL-Cs5QbLOP.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-BMdRvVEX.js → stateDiagram-v2-GCMORJYK-Bw--rdvq.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-B1wnogip.js → swimlanes-2SLR337P-B1O9IEz5.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-C6wGgYKP.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-l-3yhXUP.js → timeline-definition-EJHVYXUP-CTZ7DXaw.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-DtwtCOoJ.js → usecaseDiagram-POWQR4AR-DjwgNG0H.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-BZAEce1B.js → vennDiagram-UO4OBE2U-DrdJG55h.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-Na_BCMMO.js → wardleyDiagram-VNRHLVJA-HVq776nz.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-BW9dlUfE.js → xychartDiagram-PMCCYNJV-DPf9VEua.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/container-mcp.js +1 -1
  61. package/dist-server/index.js +3585 -3076
  62. package/dist-server/openmausbot.js +346 -159
  63. package/dist-server/pair-cli.js +346 -159
  64. package/dist-server/server/attachment-mimes.js +58 -0
  65. package/dist-server/server/attachments.js +28 -67
  66. package/dist-server/server/avatar-image.js +1 -0
  67. package/dist-server/server/avatar-subscription.js +24 -1
  68. package/dist-server/server/drivers/claude-message.js +12 -0
  69. package/dist-server/server/drivers/claude.js +9 -12
  70. package/dist-server/server/drivers/codex-image.js +141 -0
  71. package/dist-server/server/drivers/codex.js +6 -9
  72. package/dist-server/server/drivers/openai-chat.js +13 -2
  73. package/dist-server/server/engine-failover.js +40 -0
  74. package/dist-server/server/engine-failure.js +2 -2
  75. package/dist-server/server/harness-import/history.js +5 -5
  76. package/dist-server/server/harness-import/test-fixtures.js +1 -1
  77. package/dist-server/server/index.js +35 -20
  78. package/dist-server/server/message-reactions.js +11 -0
  79. package/dist-server/server/model-evidence-context.js +18 -0
  80. package/dist-server/server/model-evidence.js +145 -0
  81. package/dist-server/shared/emoji-catalog.json +1 -0
  82. package/dist-server/shared/emoji.js +27 -0
  83. package/dist-server/shared/mascot-bodies.js +9 -1
  84. package/dist-server/shared/model-evidence.js +25 -0
  85. package/dist-server/vps-container-mcp.js +1 -1
  86. package/package.json +1 -1
  87. package/dist/assets/channel-bPN03jIt.js +0 -1
  88. package/dist/assets/index-CZjRcUdn.css +0 -1
  89. package/dist/assets/swimlanesDiagram-TC7HE7FX-DWyjdGGR.js +0 -8
@@ -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";
@@ -304,7 +307,7 @@ import { createNexusMemoryRoutes } from "./routes/nexus-memory.js";
304
307
  import { createBoardRoutes } from "./routes/board.js";
305
308
  import { readBoardFromNexus } from "./board/nexus-board.js";
306
309
  import { LocalModelInventory, localModePlacement, machineMemory } from "./local-models-mode.js";
307
- 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";
308
311
  import { enterSessionFallback, onSessionFallback, ownOrFallback, sessionFallbackSpentLine, withoutSubscription } from "./session-fallback.js";
309
312
  import { TURN_ERROR_CATEGORIES } from "../shared/runtime-events.js";
310
313
  import { EngineProbes, PROBE_PROMPT, isProbeThread, probeThreadId } from "./engine-probe.js";
@@ -487,6 +490,7 @@ function customDomainStatus() {
487
490
  };
488
491
  }
489
492
  const registry = new ProviderRegistry(BUILT_IN_DRIVERS);
493
+ installAvatarSubscriptionRoute(() => registry.instances());
490
494
  // Engines installed from Settings live under the data directory and win over
491
495
  // any other copy on PATH.
492
496
  registerEnginesBinDir();
@@ -3564,6 +3568,9 @@ const roomFailoverTurns = new Map();
3564
3568
  * default (a resting engine's stand-in, a failover): the ledger books it. */
3565
3569
  const roomTurnSelections = new Map();
3566
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 });
3567
3574
  const failoverGate = new FailoverGate((turn, completion) => engineFailover(turn, completion));
3568
3575
  /** An approval shows a shell command as the model meant it: the RTK add-on's
3569
3576
  * launcher, which engines that take RTK as instructions put in front of
@@ -3582,9 +3589,16 @@ bus.setGate((published) => {
3582
3589
  return [];
3583
3590
  }
3584
3591
  const event = withoutRtkInSummary(published);
3592
+ if (!shouldIgnoreProviderEvent(event))
3593
+ modelEvidence.observe(event, () => modelExpectation(event.threadId, event.providerInstanceId, event.provider));
3585
3594
  // [jev-verifier] note a failover turn that finished, before the gate forgets it.
3586
3595
  const jevCandidate = jevAnswerCheckCandidate(event);
3587
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
+ }
3588
3602
  if (jevCandidate)
3589
3603
  setImmediate(() => void jevAnswerCheck(jevCandidate));
3590
3604
  for (const out of delivered)
@@ -3603,12 +3617,6 @@ function noteEngineBack(event) {
3603
3617
  const model = selection?.instanceId === instanceId ? selection.model : undefined;
3604
3618
  engineRests.noteBack(instanceId, candidate ? engineAccount(candidate) : undefined, candidate ? restScope(candidate, model) : undefined);
3605
3619
  }
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
3620
  /** Rest a refused engine and continue on an eligible engine, or show an actionable line. */
3613
3621
  function engineFailover(turn, completion) {
3614
3622
  const failure = turn.failure;
@@ -9825,6 +9833,7 @@ async function startTurn(botId, text, opts) {
9825
9833
  // thread, so this is the one moment the ledger's "who asked" may change.
9826
9834
  if (opts?.trigger)
9827
9835
  turnTriggers.set(threadId, opts.trigger);
9836
+ const expectedModel = modelExpectation(threadId);
9828
9837
  // The engine the ladder chose becomes the thread's model, as a person's
9829
9838
  // switch would; the thread stays tier-managed for its next turn.
9830
9839
  if (tierSelection && JSON.stringify(tierSelection) !== JSON.stringify(task.modelSelection)) {
@@ -9849,6 +9858,8 @@ async function startTurn(botId, text, opts) {
9849
9858
  // busy flips immediately so the composer locks; the dispatch itself runs
9850
9859
  // in the background — box provisioning can take ~90s and must never
9851
9860
  // hang the HTTP request
9861
+ const evidenceAttemptId = expectedModel ? modelEvidence.begin({ ...expectedModel, requestedModel: model,
9862
+ instanceId: instance.instanceId, provider: instance.driverKind }, Boolean(failover)) : undefined;
9852
9863
  const dispatchClaimId = randomUUID();
9853
9864
  const resourceOwner = { threadId, generation: dispatchClaimId };
9854
9865
  turnResourceOwners.set(threadId, resourceOwner);
@@ -10889,6 +10900,8 @@ async function startTurn(botId, text, opts) {
10889
10900
  }
10890
10901
  }
10891
10902
  catch (e) {
10903
+ if (evidenceAttemptId)
10904
+ modelEvidence.finishDispatch(threadId, evidenceAttemptId);
10892
10905
  handoffs.abandon(threadId, dispatchClaimId);
10893
10906
  failoverGate.untrack(threadId, dispatchClaimId);
10894
10907
  if (failoverTurns.get(threadId)?.generation === dispatchClaimId)
@@ -20478,14 +20491,7 @@ const handleRequest = async (req, res) => {
20478
20491
  // emoji reactions — works on any thread (1:1 or room)
20479
20492
  m = path.match(/^\/api\/threads\/([\w-]+)\/messages\/([\w-]+)\/reactions$/);
20480
20493
  if (m && method === "POST") {
20481
- const body = await readBody(req);
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 });
20494
+ return reactToMessage(store, req, res, m[1], m[2]);
20489
20495
  }
20490
20496
  if (path === "/api/sidebar-sections" && method === "GET") {
20491
20497
  return json(res, 200, { sections: store.sections });
@@ -20598,13 +20604,13 @@ const handleRequest = async (req, res) => {
20598
20604
  const profile = parseBotProfilePatch(body.profile, true);
20599
20605
  if (!profile.ok)
20600
20606
  return json(res, 400, { error: profile.error });
20601
- const checked = avatarGenerationRequestSchema.safeParse({ prompt: body.prompt });
20607
+ const checked = avatarGenerationRequestSchema.safeParse({ prompt: body.prompt, source: body.source });
20602
20608
  if (!checked.success)
20603
20609
  return json(res, 400, { error: checked.error.message });
20604
20610
  const { prompt } = checked.data;
20605
- const generated = await generateAvatarImage(cfg, {
20611
+ const generated = await generateRoutedAvatar(cfg, registry.instances(), {
20606
20612
  name: profile.patch.name ?? "", title: profile.patch.title ?? "", description: profile.patch.description ?? "",
20607
- }, prompt);
20613
+ }, prompt, checked.data.source);
20608
20614
  // A preview belongs to the draft. Upload it through the existing image
20609
20615
  // endpoint only when the user commits the new bot or its defaults.
20610
20616
  return json(res, 200, { dataUrl: `data:${generated.mime};base64,${generated.bytes.toString("base64")}` });
@@ -20855,7 +20861,7 @@ const handleRequest = async (req, res) => {
20855
20861
  if (!parsed.success) {
20856
20862
  return json(res, 400, { error: `prompt must be at most 400 characters` });
20857
20863
  }
20858
- const generated = await generateAvatarImage(cfg, existing, parsed.data.prompt);
20864
+ const generated = await generateRoutedAvatar(cfg, registry.instances(), existing, parsed.data.prompt, parsed.data.source);
20859
20865
  const current = store.bot(existing.id);
20860
20866
  if (!current)
20861
20867
  return json(res, 404, { error: "no such bot" });
@@ -21836,6 +21842,15 @@ const handleRequest = async (req, res) => {
21836
21842
  return json(res, 404, { error: "no such bot" });
21837
21843
  return json(res, 200, previewSystemPrompt(bot, { privateOwner: privateOwnerFor(auth) }));
21838
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
+ }
21839
21854
  m = path.match(/^\/api\/bots\/([\w-]+)\/model-classes$/);
21840
21855
  if (m && method === "GET") {
21841
21856
  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
+ }