@chibitek/chibi 0.1.172 → 0.1.174

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 (87) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-CGoW52MW.js → abnfDiagram-O67JEVCF-BVDndX1g.js} +1 -1
  2. package/dist/assets/{arc-DEkKLw4F.js → arc-WduS6wYB.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-Cyi6MYs6.js → architectureDiagram-NJMV4G6O-CYK9LkCf.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-CddA0wa_.js → blockDiagram-BEXU5L5S-BMj-aPW9.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-Dg8q9n90.js → c4Diagram-YGBWAQC7-cXYIStgL.js} +1 -1
  6. package/dist/assets/channel-TratrwmM.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-BSMub34c.js → chunk-2Q5K7J3B-CZ_nCppA.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW--Xc7Up5y.js → chunk-3FUC2YCW-C3wRAbtM.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-B9Ae4f30.js → chunk-5VM5RSS4-CMvgOXJP.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-Dh1To9G3.js → chunk-GTNCS2PH-9wBuLQzU.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-C_5eiV2n.js → chunk-GWA4HPMP-BpJMmfAi.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-5wQuPsks.js → chunk-JWPE2WC7-BnM06ABv.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-BGXS05ur.js → chunk-NETBCI7D-CjjiiXiT.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-ClIMhmD5.js → chunk-WEXAMYUT-D3heHHZs.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-CcKs11-L.js → chunk-XXDRQBXY-n_P41ZlM.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-BCiLckGU.js → classDiagram-v2-NBCMYWYE-Dz60Ek-6.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-JyAqT-ND.js → cose-bilkent-JH36ORCC-BCt6vBi9.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-BznovZj0.js → cynefin-OW5HDTMX-U0uq4EAQ.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-BDRrTIEB.js → cynefinDiagram-VND7K2PF-CaQX0eUJ.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-BbWguhLj.js → dagre-6A5THRUB-DlCMESYH.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-BMk1KTBr.js → diagram-22UHCM2B-Cl1xmlTD.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-ChnEP2in.js → diagram-3UASUU5V-Cus3CPg7.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-C78imnTV.js → diagram-ATOU4E4O-i06rMvae.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-Dekx02f_.js → diagram-CDSNMT55-CUM5_heN.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-D2__fPwe.js → diagram-MLGK6HIB-B5NKRruo.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-C9l7MZm7.js → diagram-MPIPVDR6-CAumAcGl.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-D4x0h3XT.js → ebnfDiagram-ZINNZB2B-B19d7d8l.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-twChz5io.js → elk-276RUBZZ-NJEpfRWi.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-BXEKWWMq.js → erDiagram-OPXOYQCR-BExk6CHc.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-Ct-XrJMr.js → flowDiagram-KWPJA3E3-ClExQ_p0.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-Bn92lxCs.js → ganttDiagram-FUAMR5RP-1MeM9sSl.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-Dvfjo7cn.js → gitGraphDiagram-X574FWY7-Tw9Dudpa.js} +1 -1
  33. package/dist/assets/{index-DvneZE6Z.js → index-KYHldywn.js} +140 -140
  34. package/dist/assets/{index-CarzpTfN.js → index-qsorLr2x.js} +1 -1
  35. package/dist/assets/{infoDiagram-VRGFBTTK-MJ14OyNX.js → infoDiagram-VRGFBTTK-ChU2tjvl.js} +1 -1
  36. package/dist/assets/{ishikawaDiagram-OU5B5YK6-nf9xhTNX.js → ishikawaDiagram-OU5B5YK6-BeTbrX2Q.js} +1 -1
  37. package/dist/assets/{journeyDiagram-ZHPQQLJL-DqfK6X2e.js → journeyDiagram-ZHPQQLJL-B7ijaWBD.js} +1 -1
  38. package/dist/assets/{kanban-definition-PNTS6WVX-CjUR1QYT.js → kanban-definition-PNTS6WVX-JBmK2F1r.js} +1 -1
  39. package/dist/assets/{linear-dPqGsZwL.js → linear-B7FXe3Ue.js} +1 -1
  40. package/dist/assets/{mermaid.core-zL0DLPAI.js → mermaid.core-BPlfZpqv.js} +5 -5
  41. package/dist/assets/{mindmap-definition-NLK3R4M7-C1-Jqg3_.js → mindmap-definition-NLK3R4M7-CmT3S5QR.js} +1 -1
  42. package/dist/assets/{pegDiagram-GJSIUBJH-CFlvuAHC.js → pegDiagram-GJSIUBJH-CFFHIcdQ.js} +1 -1
  43. package/dist/assets/{pieDiagram-5QR66LMP-BvyT0IgF.js → pieDiagram-5QR66LMP-yTiPmXRV.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-O4NWA36T-bg7JeR2c.js → quadrantDiagram-O4NWA36T-Cw8u7esm.js} +1 -1
  45. package/dist/assets/{railroadDiagram-XR7U4H2S-dF2Nqim8.js → railroadDiagram-XR7U4H2S-BfKggsOW.js} +1 -1
  46. package/dist/assets/{requirementDiagram-PLB6GJNP-CwVu_Or6.js → requirementDiagram-PLB6GJNP-Cf3n79jF.js} +1 -1
  47. package/dist/assets/{sankeyDiagram-IPEJSGJF-wBC3-mRy.js → sankeyDiagram-IPEJSGJF-B2NkSZig.js} +1 -1
  48. package/dist/assets/{sequenceDiagram-PO4LG4MO-Bs9lHMf-.js → sequenceDiagram-PO4LG4MO-BfFYiYK3.js} +1 -1
  49. package/dist/assets/{sizeCapture-INFHLROL-d6YiGOIQ.js → sizeCapture-INFHLROL-CZ_2nC3f.js} +1 -1
  50. package/dist/assets/{stateDiagram-v2-GCMORJYK--f6dscuE.js → stateDiagram-v2-GCMORJYK-B9jwzMXf.js} +1 -1
  51. package/dist/assets/{swimlanes-2SLR337P-CQpzBoAc.js → swimlanes-2SLR337P-Bxhafu77.js} +1 -1
  52. package/dist/assets/swimlanesDiagram-TC7HE7FX-DWNGCaH3.js +8 -0
  53. package/dist/assets/{timeline-definition-EJHVYXUP-DpoyUyqt.js → timeline-definition-EJHVYXUP-Dle5SJls.js} +1 -1
  54. package/dist/assets/{usecaseDiagram-POWQR4AR-Dg7mR3GW.js → usecaseDiagram-POWQR4AR-CgRTu9EO.js} +1 -1
  55. package/dist/assets/{vennDiagram-UO4OBE2U-CoEfTL7k.js → vennDiagram-UO4OBE2U-D7m_1z6B.js} +1 -1
  56. package/dist/assets/{wardleyDiagram-VNRHLVJA-24tIY01C.js → wardleyDiagram-VNRHLVJA-7-TzmX4L.js} +1 -1
  57. package/dist/assets/{xychartDiagram-PMCCYNJV-CsoqIRqn.js → xychartDiagram-PMCCYNJV-BVXXdviS.js} +1 -1
  58. package/dist/index.html +1 -1
  59. package/dist-server/index.js +2797 -1875
  60. package/dist-server/openmausbot.js +64 -2
  61. package/dist-server/pair-cli.js +64 -2
  62. package/dist-server/server/builtin-chibi.js +1 -1
  63. package/dist-server/server/dictation.js +8 -0
  64. package/dist-server/server/drivers/claude.js +4 -4
  65. package/dist-server/server/engine-tiers-view.js +49 -0
  66. package/dist-server/server/engine-tiers.js +37 -55
  67. package/dist-server/server/harness-import/engine-policy.js +6 -1
  68. package/dist-server/server/index.js +12 -13
  69. package/dist-server/server/oauth-account-core.js +153 -0
  70. package/dist-server/server/oauth-account-tools.js +7 -348
  71. package/dist-server/server/oauth-calendar-tools.js +551 -0
  72. package/dist-server/server/personal-bot.js +0 -26
  73. package/dist-server/server/remote-desktop.js +139 -7
  74. package/dist-server/server/routes/engine-tiers.js +12 -1
  75. package/dist-server/server/subscription-bots.js +2 -21
  76. package/dist-server/server/turn-resources.js +28 -0
  77. package/dist-server/server/workspace-backup-archive.js +138 -0
  78. package/dist-server/server/workspace-backup-http.js +50 -8
  79. package/dist-server/server/workspace-backup-private.js +83 -0
  80. package/dist-server/server/workspace-backup-secrets.js +90 -0
  81. package/dist-server/server/workspace-backup.js +171 -235
  82. package/dist-server/server/workspace-restore-paths.js +186 -0
  83. package/dist-server/shared/oauth-providers.js +2 -1
  84. package/dist-server/shared/provider-accounts.js +29 -0
  85. package/package.json +1 -1
  86. package/dist/assets/channel-Dda8O6gd.js +0 -1
  87. package/dist/assets/swimlanesDiagram-TC7HE7FX-C1ko4o6t.js +0 -8
@@ -19685,6 +19685,68 @@ var init_provider_accounts = __esm({
19685
19685
  config: {},
19686
19686
  keyUrl: "https://opencode.ai/go",
19687
19687
  signupUrl: "https://opencode.ai/go"
19688
+ },
19689
+ // Permanent free tiers (docs/specs/free-models-in-auto): the last resort
19690
+ // when every subscription and pay-per-use key is out (Erick's rule, a
19691
+ // model or billing error never stops the user). Off by default
19692
+ // (engineTiers.includeFree); never used for client-facing bots.
19693
+ {
19694
+ id: "openrouterFree",
19695
+ kind: "apiKey",
19696
+ label: "OpenRouter (free models)",
19697
+ billing: "free",
19698
+ driver: "openai-compat",
19699
+ envVar: "OPENROUTER_FREE_API_KEY",
19700
+ icon: "openrouter",
19701
+ config: { url: "https://openrouter.ai/api/v1", apiKeyEnv: "OPENROUTER_FREE_API_KEY", model: "", provider: "" },
19702
+ keyTest: { provider: "openaiCompat", url: "https://openrouter.ai/api/v1" },
19703
+ keyUrl: "https://openrouter.ai/keys",
19704
+ signupUrl: "https://openrouter.ai/models?max_price=0"
19705
+ },
19706
+ {
19707
+ id: "googleAiStudioFree",
19708
+ kind: "apiKey",
19709
+ label: "Google AI Studio (free tier)",
19710
+ billing: "free",
19711
+ driver: "openai-compat",
19712
+ envVar: "GOOGLE_AI_STUDIO_FREE_API_KEY",
19713
+ icon: "google",
19714
+ config: { url: "https://generativelanguage.googleapis.com/v1beta/openai", apiKeyEnv: "GOOGLE_AI_STUDIO_FREE_API_KEY", model: "", provider: "" },
19715
+ keyTest: { provider: "openaiCompat", url: "https://generativelanguage.googleapis.com/v1beta/openai" },
19716
+ keyUrl: "https://aistudio.google.com/apikey"
19717
+ },
19718
+ {
19719
+ id: "groqFree",
19720
+ kind: "apiKey",
19721
+ label: "Groq (free tier)",
19722
+ billing: "free",
19723
+ driver: "openai-compat",
19724
+ envVar: "GROQ_FREE_API_KEY",
19725
+ config: { url: "https://api.groq.com/openai/v1", apiKeyEnv: "GROQ_FREE_API_KEY", model: "", provider: "" },
19726
+ keyTest: { provider: "openaiCompat", url: "https://api.groq.com/openai/v1" },
19727
+ keyUrl: "https://console.groq.com/keys"
19728
+ },
19729
+ {
19730
+ id: "cerebrasFree",
19731
+ kind: "apiKey",
19732
+ label: "Cerebras (free tier)",
19733
+ billing: "free",
19734
+ driver: "openai-compat",
19735
+ envVar: "CEREBRAS_FREE_API_KEY",
19736
+ config: { url: "https://api.cerebras.ai/v1", apiKeyEnv: "CEREBRAS_FREE_API_KEY", model: "", provider: "" },
19737
+ keyTest: { provider: "openaiCompat", url: "https://api.cerebras.ai/v1" },
19738
+ keyUrl: "https://cloud.cerebras.ai/platform/apikeys"
19739
+ },
19740
+ {
19741
+ id: "nvidiaFree",
19742
+ kind: "apiKey",
19743
+ label: "NVIDIA NIM (free tier)",
19744
+ billing: "free",
19745
+ driver: "openai-compat",
19746
+ envVar: "NVIDIA_FREE_API_KEY",
19747
+ config: { url: "https://integrate.api.nvidia.com/v1", apiKeyEnv: "NVIDIA_FREE_API_KEY", model: "", provider: "" },
19748
+ keyTest: { provider: "openaiCompat", url: "https://integrate.api.nvidia.com/v1" },
19749
+ keyUrl: "https://build.nvidia.com/settings/api-keys"
19688
19750
  }
19689
19751
  ];
19690
19752
  }
@@ -30117,8 +30179,8 @@ var init_claude = __esm({
30117
30179
  DEFAULT_AUTOCOMPACT_TOKENS = 2e5;
30118
30180
  CLAUDE_FLAG_FLOORS = {
30119
30181
  "--strict-mcp-config": [1, 0, 60],
30120
- "--setting-sources": [1, 0, 122],
30121
- "--autocompact": [2, 1, 122],
30182
+ "--setting-sources": [1, 0, 125],
30183
+ "--autocompact": [2, 1, 221],
30122
30184
  // 2.1.267 is the first CLI that accepts it; below that the recorded prompt
30123
30185
  // simply is not refreshed, which is the pre-existing behaviour.
30124
30186
  "--system-prompt-snapshot": [2, 1, 267],
@@ -19685,6 +19685,68 @@ var init_provider_accounts = __esm({
19685
19685
  config: {},
19686
19686
  keyUrl: "https://opencode.ai/go",
19687
19687
  signupUrl: "https://opencode.ai/go"
19688
+ },
19689
+ // Permanent free tiers (docs/specs/free-models-in-auto): the last resort
19690
+ // when every subscription and pay-per-use key is out (Erick's rule, a
19691
+ // model or billing error never stops the user). Off by default
19692
+ // (engineTiers.includeFree); never used for client-facing bots.
19693
+ {
19694
+ id: "openrouterFree",
19695
+ kind: "apiKey",
19696
+ label: "OpenRouter (free models)",
19697
+ billing: "free",
19698
+ driver: "openai-compat",
19699
+ envVar: "OPENROUTER_FREE_API_KEY",
19700
+ icon: "openrouter",
19701
+ config: { url: "https://openrouter.ai/api/v1", apiKeyEnv: "OPENROUTER_FREE_API_KEY", model: "", provider: "" },
19702
+ keyTest: { provider: "openaiCompat", url: "https://openrouter.ai/api/v1" },
19703
+ keyUrl: "https://openrouter.ai/keys",
19704
+ signupUrl: "https://openrouter.ai/models?max_price=0"
19705
+ },
19706
+ {
19707
+ id: "googleAiStudioFree",
19708
+ kind: "apiKey",
19709
+ label: "Google AI Studio (free tier)",
19710
+ billing: "free",
19711
+ driver: "openai-compat",
19712
+ envVar: "GOOGLE_AI_STUDIO_FREE_API_KEY",
19713
+ icon: "google",
19714
+ config: { url: "https://generativelanguage.googleapis.com/v1beta/openai", apiKeyEnv: "GOOGLE_AI_STUDIO_FREE_API_KEY", model: "", provider: "" },
19715
+ keyTest: { provider: "openaiCompat", url: "https://generativelanguage.googleapis.com/v1beta/openai" },
19716
+ keyUrl: "https://aistudio.google.com/apikey"
19717
+ },
19718
+ {
19719
+ id: "groqFree",
19720
+ kind: "apiKey",
19721
+ label: "Groq (free tier)",
19722
+ billing: "free",
19723
+ driver: "openai-compat",
19724
+ envVar: "GROQ_FREE_API_KEY",
19725
+ config: { url: "https://api.groq.com/openai/v1", apiKeyEnv: "GROQ_FREE_API_KEY", model: "", provider: "" },
19726
+ keyTest: { provider: "openaiCompat", url: "https://api.groq.com/openai/v1" },
19727
+ keyUrl: "https://console.groq.com/keys"
19728
+ },
19729
+ {
19730
+ id: "cerebrasFree",
19731
+ kind: "apiKey",
19732
+ label: "Cerebras (free tier)",
19733
+ billing: "free",
19734
+ driver: "openai-compat",
19735
+ envVar: "CEREBRAS_FREE_API_KEY",
19736
+ config: { url: "https://api.cerebras.ai/v1", apiKeyEnv: "CEREBRAS_FREE_API_KEY", model: "", provider: "" },
19737
+ keyTest: { provider: "openaiCompat", url: "https://api.cerebras.ai/v1" },
19738
+ keyUrl: "https://cloud.cerebras.ai/platform/apikeys"
19739
+ },
19740
+ {
19741
+ id: "nvidiaFree",
19742
+ kind: "apiKey",
19743
+ label: "NVIDIA NIM (free tier)",
19744
+ billing: "free",
19745
+ driver: "openai-compat",
19746
+ envVar: "NVIDIA_FREE_API_KEY",
19747
+ config: { url: "https://integrate.api.nvidia.com/v1", apiKeyEnv: "NVIDIA_FREE_API_KEY", model: "", provider: "" },
19748
+ keyTest: { provider: "openaiCompat", url: "https://integrate.api.nvidia.com/v1" },
19749
+ keyUrl: "https://build.nvidia.com/settings/api-keys"
19688
19750
  }
19689
19751
  ];
19690
19752
  }
@@ -30117,8 +30179,8 @@ var init_claude = __esm({
30117
30179
  DEFAULT_AUTOCOMPACT_TOKENS = 2e5;
30118
30180
  CLAUDE_FLAG_FLOORS = {
30119
30181
  "--strict-mcp-config": [1, 0, 60],
30120
- "--setting-sources": [1, 0, 122],
30121
- "--autocompact": [2, 1, 122],
30182
+ "--setting-sources": [1, 0, 125],
30183
+ "--autocompact": [2, 1, 221],
30122
30184
  // 2.1.267 is the first CLI that accepts it; below that the recorded prompt
30123
30185
  // simply is not refreshed, which is the pre-existing behaviour.
30124
30186
  "--system-prompt-snapshot": [2, 1, 267],
@@ -10,7 +10,7 @@ export const CHIBI_PROFILE = {
10
10
  name: CHIBI_NAME,
11
11
  title: "Orchestrator",
12
12
  description: "Atomic's built-in orchestrator. Plans the work and delegates it to the team.",
13
- soul: "You are Chibi, Chibitek's orchestrator in Atomic. You plan the work, break it into tasks, and delegate: coding goes to the Claude and Codex bots, research to Grok. You use Mochii (the Mochii MCP server) to create, update and track tasks, and Nexus for company context. You do not write code yourself when a teammate can. Keep the person informed in plain language, confirm before anything irreversible, and call work done only after it has been verified.",
13
+ soul: "You are Chibi, Chibitek's orchestrator in Atomic. You plan the work, break it into tasks, and delegate: coding goes to the Claude and Codex bots, research to Grok. You use Mochii (the Mochii MCP server) to create, update and track tasks, and Nexus for company context. You do not write code yourself when a teammate can. Keep the person informed in plain language, confirm before anything irreversible, and call work done only after it has been verified. Every new feature the person asks for is logged as a Mochii task with the full requirements before or as it is handed to a teammate, and you send the person the task link. When the person approves an addition to an existing feature, add it to that feature's task as a comment.",
14
14
  color: CHIEF_OF_STAFF_COLOR,
15
15
  };
16
16
  export const BUILTIN_CHIBI_REFUSALS = {
@@ -84,6 +84,14 @@ export function checkCleanup(input, output) {
84
84
  if (tokens.filter((t) => kept.has(t)).length / tokens.length < 0.6)
85
85
  return { ok: false, reason: "overlap" };
86
86
  }
87
+ // a correction swaps a misheard word for the right one; words with nothing
88
+ // taken out for them were never said
89
+ const spoken = new Set(contentTokens(input));
90
+ const written = new Set(contentTokens(cleaned));
91
+ const added = [...written].filter((t) => !spoken.has(t)).length;
92
+ const removed = [...spoken].filter((t) => !written.has(t)).length;
93
+ if (added > removed + 1)
94
+ return { ok: false, reason: "added" };
87
95
  return { ok: true, text: cleaned };
88
96
  }
89
97
  export async function ask(provider, prompt, timeoutMs) {
@@ -259,12 +259,12 @@ const DEFAULT_AUTOCOMPACT_TOKENS = 200_000;
259
259
  *
260
260
  * Verified against the published binaries, not the changelog (which never
261
261
  * records `--autocompact`): `--strict-mcp-config` is present in 1.0.60 and
262
- * absent from 1.0.0; `--setting-sources` first appears in 1.0.122 (1.0.120
263
- * lacks it); `--autocompact` first appears in 2.1.122 (2.1.121 lacks it). */
262
+ * absent from 1.0.0; `--setting-sources` first appears in 1.0.125 (1.0.124
263
+ * lacks it); `--autocompact` first appears in 2.1.221 (2.1.220 lacks it). */
264
264
  export const CLAUDE_FLAG_FLOORS = {
265
265
  "--strict-mcp-config": [1, 0, 60],
266
- "--setting-sources": [1, 0, 122],
267
- "--autocompact": [2, 1, 122],
266
+ "--setting-sources": [1, 0, 125],
267
+ "--autocompact": [2, 1, 221],
268
268
  // 2.1.267 is the first CLI that accepts it; below that the recorded prompt
269
269
  // simply is not refreshed, which is the pre-existing behaviour.
270
270
  "--system-prompt-snapshot": [2, 1, 267],
@@ -0,0 +1,49 @@
1
+ // The Settings view of engine tiers (docs/chibitek/engine-tiers.md): every
2
+ // tier's ladder with health and rest, the installed engines, rest history,
3
+ // pay-per-use episodes and the "Include free models in Auto" setting.
4
+ import { BOT_TIERS, DEFAULT_LADDERS, DEFAULT_TIER_CLASSES } from "../shared/engine-tiers.js";
5
+ import { engineHealth, engineTierClasses, engineTierLadders, engineTiersIncludeFree, keyBased, matchesEngine } from "./engine-tiers.js";
6
+ import { perTokenModel } from "./harness-import/engine-policy.js";
7
+ const DRIVER_NAMES = {
8
+ "atomic-engine": "Atomic Engine", claudeAgent: "Claude", codex: "Codex", cursorAgent: "Cursor", kimiAgent: "Kimi",
9
+ grokAgent: "Grok", vibeAgent: "Mistral Vibe", antigravityAgent: "Gemini (Antigravity)", qwenAgent: "Qwen (local)", opencodeGo: "OpenCode",
10
+ hermesAgent: "Hermes", ollamaCloud: "Ollama Cloud", mistral: "Mistral", "openai-compat": "OpenAI-compatible endpoint",
11
+ appleIntelligence: "Apple Intelligence (on this Mac)",
12
+ };
13
+ /** The Settings view: every tier's ladder with each rung's health and rest,
14
+ * the installed engines a person may add, and the recent rest history. */
15
+ export function engineTiersView(engineTiers, candidates, rests, defaults = DEFAULT_LADDERS) {
16
+ const ladders = engineTierLadders(engineTiers, defaults);
17
+ const classes = engineTierClasses(engineTiers);
18
+ const restOf = (candidate) => {
19
+ const rest = rests?.rest(candidate);
20
+ return rest ? { rest } : {};
21
+ };
22
+ const engines = candidates.filter((candidate) => candidate.driverKind !== "boxAgent").map((candidate) => ({
23
+ instanceId: candidate.instanceId, driverKind: candidate.driverKind, label: candidate.displayName,
24
+ health: engineHealth(candidate), paid: keyBased(candidate),
25
+ models: [...new Map([candidate.models.default, ...candidate.models.options.map((option) => option.id)].filter(Boolean).map((id) => {
26
+ const option = candidate.models.options.find((item) => item.id === id);
27
+ return [id, { id, label: option?.label || id, paid: perTokenModel(candidate, id) }];
28
+ })).values()],
29
+ ...(candidate.models.default ? { defaultModel: candidate.models.default } : {}), ...restOf(candidate),
30
+ }));
31
+ const rung = (entry) => {
32
+ const matching = candidates.filter((candidate) => matchesEngine(entry, candidate));
33
+ const best = matching.find((candidate) => engineHealth(candidate) === "ready" && !rests?.rest(candidate)) ??
34
+ matching.find((candidate) => engineHealth(candidate) === "ready") ?? matching[0];
35
+ const name = best?.displayName ?? DRIVER_NAMES[entry.engine] ?? entry.engine;
36
+ return { ...entry, label: entry.model ? `${name} (${entry.model})` : name, ...(best ? { instanceId: best.instanceId } : {}),
37
+ health: best ? engineHealth(best) : "unavailable", paid: best ? keyBased(best) : false, ...(best ? restOf(best) : {}) };
38
+ };
39
+ const label = (instanceId) => candidates.find((candidate) => candidate.instanceId === instanceId)?.displayName ?? instanceId;
40
+ return {
41
+ tiers: BOT_TIERS.map((tier) => ({ tier, modelClass: classes[tier], classCustom: classes[tier] !== DEFAULT_TIER_CLASSES[tier],
42
+ ladder: ladders[tier].map(rung), custom: JSON.stringify(ladders[tier]) !== JSON.stringify(defaults[tier]) })),
43
+ engines,
44
+ ...(engineTiersIncludeFree(engineTiers) ? { includeFree: true } : {}),
45
+ ...(rests?.payPerUse ? { payPerUse: rests.payPerUse() } : {}),
46
+ ...(rests ? { restHistory: rests.history().map((record) => ({ ...record,
47
+ label: record.scope ? `${label(record.instanceId)} (${record.scope})` : label(record.instanceId) })) } : {}),
48
+ };
49
+ }
@@ -7,7 +7,7 @@ import { BOT_TIERS, DEFAULT_LADDERS, DEFAULT_TIER_CLASSES, MODEL_CLASSES, classB
7
7
  import { modelHost } from "../shared/engine-choices.js";
8
8
  export { DEFAULT_LADDERS };
9
9
  import { subscriptionProviderForDriver } from "../shared/subscription-bots.js";
10
- import { ATOMIC_ENGINE_INSTANCE, engineIntent, localModel, modelBilling, modelProvider, paidCandidate, perTokenModel } from "./harness-import/engine-policy.js";
10
+ import { ATOMIC_ENGINE_INSTANCE, engineIntent, freeCandidate, localModel, modelBilling, modelProvider, paidCandidate, perTokenModel } from "./harness-import/engine-policy.js";
11
11
  /** Engines a tier tries first, whatever the class catalog order says: Chibi
12
12
  * runs on Claude (Erick, 2026-10-13: open-weight models broke its
13
13
  * orchestration). Its ladder starts with Claude's best model graded in its
@@ -169,6 +169,13 @@ export function engineTierClasses(engineTiers) {
169
169
  const classes = stored && typeof stored === "object" && !Array.isArray(stored) ? stored : {};
170
170
  return Object.fromEntries(BOT_TIERS.map((tier) => [tier, toModelClass(classes[tier]) ?? DEFAULT_TIER_CLASSES[tier]]));
171
171
  }
172
+ /** A bot counts as handling client data unless switched off (WireBot.clientData),
173
+ * so a free-tier model is never the default for one. */
174
+ export const handlesClientData = (bot) => bot.clientData !== false;
175
+ /** "Include free models in Auto" (config.json engineTiers.includeFree): off by default. */
176
+ export function engineTiersIncludeFree(engineTiers) {
177
+ return configObject(engineTiers).includeFree === true;
178
+ }
172
179
  function storedClasses(engineTiers) {
173
180
  const current = engineTierClasses(engineTiers);
174
181
  return Object.fromEntries(BOT_TIERS.filter((tier) => current[tier] !== DEFAULT_TIER_CLASSES[tier]).map((tier) => [tier, current[tier]]));
@@ -224,7 +231,7 @@ export function engineHealth(candidate) {
224
231
  return "signed-out";
225
232
  return candidate.models.default || candidate.models.options.length ? "ready" : "unavailable";
226
233
  }
227
- const matchesEngine = (entry, candidate) => entry.engine === candidate.instanceId || entry.engine === candidate.driverKind;
234
+ export const matchesEngine = (entry, candidate) => entry.engine === candidate.instanceId || entry.engine === candidate.driverKind;
228
235
  /** The model a rung runs on this engine: its exact model id, else the first
229
236
  * id containing it, else (no model named) the engine's default. Where several
230
237
  * ids contain it (one model offered by many providers on Hermes), one that is
@@ -274,10 +281,11 @@ export function resolveLadder(ladder, stored, context) {
274
281
  const skipped = [];
275
282
  const subscription = [];
276
283
  const key = [];
284
+ const free = [];
277
285
  // The engine the bot is already on goes first among equals.
278
286
  const storedFirst = (a, b) => Number(b.instanceId === stored.instanceId) - Number(a.instanceId === stored.instanceId);
279
287
  for (const [step, entry] of ladder.entries()) {
280
- const matching = context.candidates.filter((candidate) => !keyBased(candidate) && matchesEngine(entry, candidate));
288
+ const matching = context.candidates.filter((candidate) => !keyBased(candidate) && !freeCandidate(candidate) && matchesEngine(entry, candidate));
281
289
  for (const candidate of matching.sort(storedFirst)) {
282
290
  const band = perTokenModel(candidate, entryModel(entry, candidate)) ? "key" : "subscription";
283
291
  (band === "key" ? key : subscription).push({ step, entry, candidate, band, index: context.candidates.indexOf(candidate) });
@@ -293,6 +301,14 @@ export function resolveLadder(ladder, stored, context) {
293
301
  if (candidate.driverKind === "ollamaCloud" && !keyBased(candidate) && !subscription.some((attempt) => attempt.candidate === candidate)) {
294
302
  subscription.push({ step: ladder.length, entry: { engine: candidate.instanceId }, candidate, band: "subscription", index });
295
303
  }
304
+ // A free-tier account joins every ladder by itself, in its own
305
+ // last-resort band (gated by includeFree/clientData below): no rung
306
+ // ever names one, same as how a key-based engine joins automatically.
307
+ if (freeCandidate(candidate)) {
308
+ const step = rungOf(candidate);
309
+ free.push({ step, entry: ladder[step] ?? { engine: candidate.instanceId }, candidate, band: "free", index });
310
+ continue;
311
+ }
296
312
  if (!keyBased(candidate) || candidate.driverKind === "boxAgent")
297
313
  continue;
298
314
  const step = rungOf(candidate);
@@ -300,8 +316,12 @@ export function resolveLadder(ladder, stored, context) {
300
316
  }
301
317
  subscription.sort((a, b) => atomicFirst(a.candidate) - atomicFirst(b.candidate) || a.step - b.step);
302
318
  key.sort((a, b) => a.step - b.step || storedFirst(a.candidate, b.candidate) || a.index - b.index);
303
- for (const { step, entry, candidate, band } of [...subscription, ...key]) {
304
- const reason = band === "key" && context.keyBandPaused ? "paused" : skipReason(entry, candidate, context, step < ladder.length);
319
+ free.sort((a, b) => a.step - b.step || storedFirst(a.candidate, b.candidate) || a.index - b.index);
320
+ for (const { step, entry, candidate, band } of [...subscription, ...key, ...free]) {
321
+ const reason = band === "key" && context.keyBandPaused ? "paused"
322
+ : band === "free" && !context.includeFree ? "free-off"
323
+ : band === "free" && context.clientData ? "client-data"
324
+ : skipReason(entry, candidate, context, step < ladder.length);
305
325
  if (reason) {
306
326
  skipped.push({ step, instanceId: candidate.instanceId, reason, band });
307
327
  continue;
@@ -310,7 +330,7 @@ export function resolveLadder(ladder, stored, context) {
310
330
  const keep = candidate.instanceId === stored.instanceId &&
311
331
  (!entry.model || stored.model.toLowerCase().includes(entry.model.toLowerCase())) &&
312
332
  (stored.model === candidate.models.default || candidate.models.options.some((option) => option.id === stored.model)) &&
313
- (band === "key" || !perTokenModel(candidate, stored.model));
333
+ (band !== "subscription" || !perTokenModel(candidate, stored.model));
314
334
  return { choice: { selection: keep ? { ...stored } : { instanceId: candidate.instanceId, model }, candidate, step, band }, skipped };
315
335
  }
316
336
  return { skipped };
@@ -362,7 +382,7 @@ export function resolveFailover(request) {
362
382
  // a turn falls to any of them, not only to those its ladders name.
363
383
  const extras = new Map();
364
384
  for (const candidate of request.context.candidates) {
365
- if (keyBased(candidate) || candidate.driverKind === "boxAgent")
385
+ if (keyBased(candidate) || freeCandidate(candidate) || candidate.driverKind === "boxAgent")
366
386
  continue;
367
387
  // An engine that runs many providers (Hermes, Atomic Engine) offers each
368
388
  // flat plan's models; its key-billed and unlabelled ones are not a plan.
@@ -390,15 +410,19 @@ export function resolveFailover(request) {
390
410
  };
391
411
  const ladders = new Map(classes.map((modelClass) => [modelClass, ladderOf(modelClass)]));
392
412
  const all = [];
393
- let keyPassSkipped;
394
- for (const keys of [false, true]) {
413
+ let lastPassSkipped;
414
+ for (const stage of ["subscription", "key", "free"]) {
395
415
  for (const modelClass of classes) {
396
416
  const { ladder, vouched } = ladders.get(modelClass);
397
417
  const effective = (candidate, model, entry, step) => entry?.modelClass ?? request.classOf(candidate, model) ?? (step < vouched ? modelClass : "Añejo");
398
418
  const accept = (candidate, model, entry, named) => {
399
419
  if (request.tried.has(candidate.instanceId))
400
420
  return "tried";
401
- if (!keys && (keyBased(candidate) || perTokenModel(candidate, model)))
421
+ // Free is the last-resort band: deferred past subscriptions and keys
422
+ // in every class, same as a key engine is deferred past subscriptions.
423
+ if (stage !== "free" && freeCandidate(candidate))
424
+ return "key-later";
425
+ if (stage === "subscription" && (keyBased(candidate) || perTokenModel(candidate, model)))
402
426
  return "key-later";
403
427
  // a ladder-named local model is a normal rung (Erick, 2026-10-24)
404
428
  if (!named && request.isLocal?.(candidate, model) && !(request.localAllowed && modelClass === "Añejo"))
@@ -408,8 +432,8 @@ export function resolveFailover(request) {
408
432
  };
409
433
  const resolution = resolveLadder(ladder, request.failed, { ...request.context, accept });
410
434
  all.push(...resolution.skipped);
411
- if (keys && modelClass === request.modelClass)
412
- keyPassSkipped = resolution.skipped;
435
+ if (stage === "free" && modelClass === request.modelClass)
436
+ lastPassSkipped = resolution.skipped;
413
437
  if (resolution.choice) {
414
438
  const { choice } = resolution;
415
439
  const named = choice.step < ladder.length;
@@ -418,7 +442,7 @@ export function resolveFailover(request) {
418
442
  }
419
443
  }
420
444
  }
421
- return { skipped: keyPassSkipped ? mergeSkipped([...keyPassSkipped, ...all]) : mergeSkipped(all) };
445
+ return { skipped: lastPassSkipped ? mergeSkipped([...lastPassSkipped, ...all]) : mergeSkipped(all) };
422
446
  }
423
447
  /** One skip per engine, the most telling reason kept: a resting, paused or
424
448
  * refused engine says more than "not this class" or "a key, later". */
@@ -441,50 +465,8 @@ export function engineLabel(candidates, selection) {
441
465
  const model = candidate?.models.options.find((option) => option.id === selection.model)?.label ?? selection.model;
442
466
  return model && model !== name ? `${name} · ${model}` : name;
443
467
  }
444
- const DRIVER_NAMES = {
445
- "atomic-engine": "Atomic Engine", claudeAgent: "Claude", codex: "Codex", cursorAgent: "Cursor", kimiAgent: "Kimi",
446
- grokAgent: "Grok", vibeAgent: "Mistral Vibe", antigravityAgent: "Gemini (Antigravity)", qwenAgent: "Qwen (local)", opencodeGo: "OpenCode",
447
- hermesAgent: "Hermes", ollamaCloud: "Ollama Cloud", mistral: "Mistral", "openai-compat": "OpenAI-compatible endpoint",
448
- appleIntelligence: "Apple Intelligence (on this Mac)",
449
- };
450
468
  /** The account an engine is signed in to, as resting keys it. */
451
469
  export const engineAccount = (candidate) => candidate.snapshot.account?.email ?? candidate.snapshot.account?.organization;
452
- /** The Settings view: every tier's ladder with each rung's health and rest,
453
- * the installed engines a person may add, and the recent rest history. */
454
- export function engineTiersView(engineTiers, candidates, rests, defaults = DEFAULT_LADDERS) {
455
- const ladders = engineTierLadders(engineTiers, defaults);
456
- const classes = engineTierClasses(engineTiers);
457
- const restOf = (candidate) => {
458
- const rest = rests?.rest(candidate);
459
- return rest ? { rest } : {};
460
- };
461
- const engines = candidates.filter((candidate) => candidate.driverKind !== "boxAgent").map((candidate) => ({
462
- instanceId: candidate.instanceId, driverKind: candidate.driverKind, label: candidate.displayName,
463
- health: engineHealth(candidate), paid: keyBased(candidate),
464
- models: [...new Map([candidate.models.default, ...candidate.models.options.map((option) => option.id)].filter(Boolean).map((id) => {
465
- const option = candidate.models.options.find((item) => item.id === id);
466
- return [id, { id, label: option?.label || id, paid: perTokenModel(candidate, id) }];
467
- })).values()],
468
- ...(candidate.models.default ? { defaultModel: candidate.models.default } : {}), ...restOf(candidate),
469
- }));
470
- const rung = (entry) => {
471
- const matching = candidates.filter((candidate) => matchesEngine(entry, candidate));
472
- const best = matching.find((candidate) => engineHealth(candidate) === "ready" && !rests?.rest(candidate)) ??
473
- matching.find((candidate) => engineHealth(candidate) === "ready") ?? matching[0];
474
- const name = best?.displayName ?? DRIVER_NAMES[entry.engine] ?? entry.engine;
475
- return { ...entry, label: entry.model ? `${name} (${entry.model})` : name, ...(best ? { instanceId: best.instanceId } : {}),
476
- health: best ? engineHealth(best) : "unavailable", paid: best ? keyBased(best) : false, ...(best ? restOf(best) : {}) };
477
- };
478
- const label = (instanceId) => candidates.find((candidate) => candidate.instanceId === instanceId)?.displayName ?? instanceId;
479
- return {
480
- tiers: BOT_TIERS.map((tier) => ({ tier, modelClass: classes[tier], classCustom: classes[tier] !== DEFAULT_TIER_CLASSES[tier],
481
- ladder: ladders[tier].map(rung), custom: JSON.stringify(ladders[tier]) !== JSON.stringify(defaults[tier]) })),
482
- engines,
483
- ...(rests?.payPerUse ? { payPerUse: rests.payPerUse() } : {}),
484
- ...(rests ? { restHistory: rests.history().map((record) => ({ ...record,
485
- label: record.scope ? `${label(record.instanceId)} (${record.scope})` : label(record.instanceId) })) } : {}),
486
- };
487
- }
488
470
  /** One bot's rebalance preview row. `refuse` carries the same per-bot
489
471
  * refusals a turn would apply (policy, approval level). */
490
472
  export function rebalanceRow(bot, engineTiers, candidates, refuse, defaults = DEFAULT_LADDERS) {
@@ -54,7 +54,8 @@ const ready = (candidate) => candidate.snapshot.state === "available" && candida
54
54
  * Plan, is a subscription); Ollama Cloud, a flat plan used through a key;
55
55
  * else a metered snapshot, a per-token driver or a sign-in with an API key.
56
56
  * An engine Atomic cannot tell about (Cursor, Antigravity) is not labelled;
57
- * it still ranks after the subscription list. */
57
+ * it still ranks after the subscription list. A free-tier account
58
+ * (billing: "free") is never pay per token. */
58
59
  export const paidCandidate = (candidate) => {
59
60
  if (candidate.billing)
60
61
  return candidate.billing === "payPerUse";
@@ -63,6 +64,10 @@ export const paidCandidate = (candidate) => {
63
64
  return candidate.snapshot.billing === "metered" || PER_TOKEN_KINDS.has(candidate.driverKind) ||
64
65
  candidate.snapshot.account?.method === "api-key";
65
66
  };
67
+ /** A provider account added as a permanent free tier (shared/
68
+ * provider-accounts.ts, billing: "free"): the last-resort band in
69
+ * resolveLadder, never the subscription or key band. */
70
+ export const freeCandidate = (candidate) => candidate.billing === "free";
66
71
  // ── billing per model, on engines that run many providers ────────────────
67
72
  // Hermes, and Atomic Engine (built on it), run models from every provider
68
73
  // they are configured for, as `<provider>:<model>` ("deepinfra:XiaomiMiMo/
@@ -145,7 +145,7 @@ import { buildRecoveryText, buildTurnContext, engineIsFresh, NATIVELY_REPLAYING_
145
145
  import { Handoffs, handedStateUsable, recordHanded, renderUnseen, sessionStart, unseenMessages, withUnseenMessages } from "./delta-context.js";
146
146
  import { extractTurnImages } from "./turn-images.js";
147
147
  import { TurnWatchdog } from "./turn-watchdog.js";
148
- import { TurnResources, workspaceResource } from "./turn-resources.js";
148
+ import { TurnResources, folderHeldElsewhere, queuedTurnFolder, workspaceResource } from "./turn-resources.js";
149
149
  import { ensureWorkspace, ensureTaskWorkspace, workspaceLocationsPrompt, supportsWorkspaceFiles, updateMemory, appendMemoryLog, isMemoryTopicName, loadMemory, memorySystemPrompt, memorySourceLabel, searchMemoryFiles, SESSION_SEARCH_SYSTEM_PROMPT, TASK_WORKSPACES_DIR, WORKSPACES_DIR, workspaceDir, } from "./workspace.js";
150
150
  import { fromOwner, NexusMemory, surfaceForRequest } from "./nexus-memory/index.js";
151
151
  import { formatNexusFile, formatNexusRecall } from "./nexus-memory/tool-text.js";
@@ -275,6 +275,7 @@ import { accountStops, injectedModel, restScope } from "./injected-model-rest.js
275
275
  import { accountProvider } from "../shared/provider-accounts.js";
276
276
  import { createAccountRoutes } from "./routes/accounts.js";
277
277
  import { installNativeVault } from "./vault/runtime.js";
278
+ import { installWorkspaceSecrets } from "./workspace-backup-secrets.js";
278
279
  import { OAuthAccounts, takeOAuthClients } from "./oauth-accounts.js";
279
280
  import { DesktopAccountVault, FileAccountVault, UnavailableAccountVault, takeAccountsFromEnvironment } from "./oauth-account-vault.js";
280
281
  import { accountConnectorNames } from "./claude-account-connectors.js";
@@ -289,7 +290,7 @@ import { HISTORY_RECEIPT_KIND, historyAttachmentStore } from "./harness-import/h
289
290
  import { chooseEngine, engineIntent, harnessForName, localModel, newBotSelection, paidCandidate, perTokenModel } from "./harness-import/engine-policy.js";
290
291
  import { createEngineTierRoutes } from "./routes/engine-tiers.js";
291
292
  import { createPrimaryBotRoutes } from "./routes/primary-bot.js";
292
- import { EngineSnapshotCache, botTier, canDriveComputer, classLadder, engineAccount, engineHealth, engineLabel, engineTierClasses, engineTierLadders, generateLadders, ladderForClass, keyBased, needsDelegation, resolveFailover, resolveLadder } from "./engine-tiers.js";
293
+ import { EngineSnapshotCache, botTier, canDriveComputer, classLadder, engineAccount, engineHealth, engineLabel, engineTierClasses, engineTierLadders, engineTiersIncludeFree, generateLadders, handlesClientData, ladderForClass, keyBased, needsDelegation, resolveFailover, resolveLadder } from "./engine-tiers.js";
293
294
  import { PRIMARY_BOT_THREAD_TITLE, PRIMARY_BOT_TRIGGER, PrimaryBotStore, routesAsSweep, PrimaryBotSweeper } from "./primary-bot.js";
294
295
  import { PRIMARY_BOT_LOCKED, primaryBotChoiceField } from "../shared/primary-bot.js";
295
296
  import { moveIntoChibi } from "./move-into-chibi.js";
@@ -381,13 +382,10 @@ function releaseDataDirLeaseAtExit() {
381
382
  process.once("exit", releaseDataDirLeaseAtExit);
382
383
  // Restore before constructing any long-lived config, Store, session or provider
383
384
  // objects. Replacing files underneath a live Store would overwrite restored data.
384
- let workspaceRestore = { restored: false };
385
- if (existsSync(join(DATA_DIR, ".backups"))) {
386
- workspaceRestore = applyPendingWorkspaceRestore(DATA_DIR);
387
- if (!workspaceRestore.restored && !workspaceRestore.rolledBack) {
388
- workspaceRestore = readLastWorkspaceRestore(DATA_DIR) ?? workspaceRestore;
389
- }
390
- }
385
+ const restoredThisBoot = existsSync(join(DATA_DIR, ".backups")) ? applyPendingWorkspaceRestore(DATA_DIR) : { restored: false };
386
+ let workspaceRestore = restoredThisBoot;
387
+ if (existsSync(join(DATA_DIR, ".backups")) && !workspaceRestore.restored && !workspaceRestore.rolledBack)
388
+ workspaceRestore = readLastWorkspaceRestore(DATA_DIR) ?? workspaceRestore;
391
389
  const workspaceMaintenance = new WorkspaceBackupMaintenance();
392
390
  // Only after ensureDirs(): it performs the one-time rename of the legacy data
393
391
  // dir, which must not find a freshly created ~/.openmausbot already there.
@@ -1176,6 +1174,7 @@ const availableSkills = () => mergeSkills(bundledSkills, loadUserSkills(join(DAT
1176
1174
  // supplies parentPort; plain Node intentionally leaves it absent.
1177
1175
  const utilityParentPort = process.parentPort;
1178
1176
  installNativeVault(DATA_DIR, utilityParentPort, () => desktopMutationToken, req => authorizedInternalCapability(req.headers.authorization) !== null);
1177
+ installWorkspaceSecrets(utilityParentPort, DATA_DIR, restoredThisBoot);
1179
1178
  function postDesktopPrivateMessage(message) {
1180
1179
  if (!utilityParentPort)
1181
1180
  return false;
@@ -3103,7 +3102,7 @@ function ladderTurnEngineChoice(profile, projected, threadId, needs, runOn, subs
3103
3102
  const { ladder } = plan;
3104
3103
  const computer = turnComputer(projected, threadId, runOn);
3105
3104
  const context = { candidates, needsDelegation: needsDelegation(tier, profile), computer, refuse: engineTierRefusal(projected), resting: engineResting,
3106
- keyBandPaused: subscriptionsOnly || payPerUse.pausedUntil() !== undefined };
3105
+ keyBandPaused: subscriptionsOnly || payPerUse.pausedUntil() !== undefined, includeFree: engineTiersIncludeFree(cfg.engineTiers), clientData: handlesClientData(profile) };
3107
3106
  const local = (resolution) => localModeTurn(resolution, { tier, context, needs, prefer: [projected.modelSelection.instanceId] });
3108
3107
  const wider = (tried, modelClass) => resolveFailover({
3109
3108
  ladder, classLadder: (cls) => classLadder(classedModels(), cls), failed: projected.modelSelection, context, tried,
@@ -3876,7 +3875,7 @@ function failoverPick(turn, profile, projected, candidates, failedCandidate, res
3876
3875
  classLadder: (modelClass) => classLadder(classedModels(), modelClass),
3877
3876
  failed: attempt.selection,
3878
3877
  context: { candidates, needsDelegation: needsDelegation(tier, profile), computer, refuse: engineTierRefusal(projected), resting: engineResting,
3879
- keyBandPaused: Boolean(fallback) || keyBandPausedFor(turn.threadId) },
3878
+ keyBandPaused: Boolean(fallback) || keyBandPausedFor(turn.threadId), includeFree: engineTiersIncludeFree(cfg.engineTiers), clientData: handlesClientData(profile) },
3880
3879
  tried,
3881
3880
  modelClass: neededClass,
3882
3881
  mayDrop: true,
@@ -5347,7 +5346,7 @@ const roomHandoffs = new RoomHandoffs(join(DATA_DIR, "room-handoffs.json"), {
5347
5346
  busy: n => {
5348
5347
  if (n.groupId)
5349
5348
  return Boolean(store.bot(n.botId)?.busy || (store.group(n.groupId) && groupIsWorking(store.group(n.groupId))));
5350
- const slot = threadBusy(n.botId, n.threadId) || botAtThreadCapacity(n.botId) || Boolean(activeGroupTurnForBot(n.botId));
5349
+ const slot = threadBusy(n.botId, n.threadId) || botAtThreadCapacity(n.botId) || Boolean(activeGroupTurnForBot(n.botId)) || folderHeldElsewhere(turnResources, queuedTurnFolder(store.taskByThread(n.botId, n.threadId)?.cwd, repoThreadStarted(n.threadId), turnRepoPlan(n.threadId), store.bot(n.botId)?.cwd), n.threadId);
5351
5350
  return n.status === "resume" ? slot : slot || recipientAwaitingPerson(n.botId, n.threadId);
5352
5351
  },
5353
5352
  changed: (groupIds, directThreadIds) => {
@@ -21004,7 +21003,7 @@ const handleRequest = async (req, res) => {
21004
21003
  section = trimmed;
21005
21004
  }
21006
21005
  }
21007
- for (const key of ["unread", "cloudBackend", "color", "mascotExpression", "mascotBody", "pinned", "hidden"]) {
21006
+ for (const key of ["unread", "cloudBackend", "color", "mascotExpression", "mascotBody", "pinned", "hidden", "clientData"]) {
21008
21007
  if (body[key] !== undefined)
21009
21008
  patch[key] = body[key];
21010
21009
  }