@chibitek/chibi 0.1.155 → 0.1.158

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 (98) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-B8LdsCr1.js → abnfDiagram-O67JEVCF-B8-8zzMS.js} +1 -1
  2. package/dist/assets/{arc-DU5fSmKU.js → arc-CSHQ9Dx3.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-DdQSIFKv.js → architectureDiagram-NJMV4G6O-BCHM9rXW.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-CaPcGYSS.js → blockDiagram-BEXU5L5S-B-5uxPFN.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-unt0ePgO.js → c4Diagram-YGBWAQC7-CuKNM_MR.js} +1 -1
  6. package/dist/assets/channel-bPN03jIt.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-Babg5Fzu.js → chunk-2Q5K7J3B-BNF02GM4.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-D1-dWVHo.js → chunk-3FUC2YCW-uEWgBd4M.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-DU7nSMbT.js → chunk-5VM5RSS4-eSZuXctY.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-QIjCJIhV.js → chunk-GTNCS2PH-CslS8nRS.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-pSByUE5R.js → chunk-GWA4HPMP-w71-MeKj.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-Dj2AaR1b.js → chunk-JWPE2WC7-Bjju7WGh.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-pZbN0-FP.js → chunk-NETBCI7D-HxrroD4M.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-Bqarwe4D.js → chunk-WEXAMYUT-D7Rz470x.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-WV8lm3aa.js → chunk-XXDRQBXY-BWKnAeyM.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-BANBtky1.js → classDiagram-v2-NBCMYWYE-CeBDRHmP.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-CtrktgBC.js → cose-bilkent-JH36ORCC-BBbRKlvr.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-DnhVVfba.js → cynefin-OW5HDTMX-W02cnJ0B.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-B42SRR6x.js → cynefinDiagram-VND7K2PF-Cs47eaxd.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-DM--vFMK.js → dagre-6A5THRUB-8mG18hzT.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-ZBupBrPU.js → diagram-22UHCM2B-DF4Qrw67.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-BPQzNIOd.js → diagram-3UASUU5V-3BAVoed1.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-5kO8vf8Z.js → diagram-ATOU4E4O-CKO3pECE.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-DtxogB86.js → diagram-CDSNMT55-CZhrGvur.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-Co9-nYw_.js → diagram-MLGK6HIB-C07l9pdQ.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-DWRuMh5y.js → diagram-MPIPVDR6-D3or4lkZ.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-DU_nmoG9.js → ebnfDiagram-ZINNZB2B-DV2epXp_.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-Bcje7S94.js → elk-276RUBZZ-DlvEn4-1.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-9aCo_2CI.js → erDiagram-OPXOYQCR-CB6X7Wt5.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-D4FYFyc5.js → flowDiagram-KWPJA3E3-C3bqz4lC.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-WQHX5rbx.js → ganttDiagram-FUAMR5RP-yo_Evhab.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-DRyzBdiV.js → gitGraphDiagram-X574FWY7-Cq2oUlap.js} +1 -1
  33. package/dist/assets/{index-CG1OvRgO.js → index-CG4AFPb4.js} +1 -1
  34. package/dist/assets/{index-D_8SD_0K.js → index-CMcr5k3Z.js} +205 -200
  35. package/dist/assets/index-CZjRcUdn.css +1 -0
  36. package/dist/assets/{infoDiagram-VRGFBTTK-D7SOmp4n.js → infoDiagram-VRGFBTTK-Y3ZrY7qL.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CdxOnDVX.js → ishikawaDiagram-OU5B5YK6-BpOtSfyi.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-CH7477Oa.js → journeyDiagram-ZHPQQLJL-UQM727le.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-BdpVvkHd.js → kanban-definition-PNTS6WVX-C3s5oHpY.js} +1 -1
  40. package/dist/assets/{linear-DTGq6qn5.js → linear-hX6nFOQr.js} +1 -1
  41. package/dist/assets/{mermaid.core-Sn2sv0Ti.js → mermaid.core-DZObGLN7.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-koCmkYuq.js → mindmap-definition-NLK3R4M7-Br-i2E3P.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-C3sqhm8e.js → pegDiagram-GJSIUBJH-D5SERB-N.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-BELBK122.js → pieDiagram-5QR66LMP-BPNf4J3D.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-Cksi5LSm.js → quadrantDiagram-O4NWA36T-Bm_Wh7Jr.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-f1av_EZW.js → railroadDiagram-XR7U4H2S-D6bo0abD.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-CW4O5nER.js → requirementDiagram-PLB6GJNP--1n5rkV7.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF-Dz8ADHNY.js → sankeyDiagram-IPEJSGJF-Ct4tP2kK.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-Cc3HE0iZ.js → sequenceDiagram-PO4LG4MO-CCG7O5fR.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-gdaZNHnK.js → sizeCapture-INFHLROL-CJJJra9c.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK--u8LCnab.js → stateDiagram-v2-GCMORJYK-BMdRvVEX.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-4MAw4kdO.js → swimlanes-2SLR337P-B1wnogip.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-DWyjdGGR.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-CL89YkCT.js → timeline-definition-EJHVYXUP-l-3yhXUP.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-C92l7YQe.js → usecaseDiagram-POWQR4AR-DtwtCOoJ.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-17siYhSj.js → vennDiagram-UO4OBE2U-BZAEce1B.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-CNwa9VmN.js → wardleyDiagram-VNRHLVJA-Na_BCMMO.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-Dnu6Xzih.js → xychartDiagram-PMCCYNJV-BW9dlUfE.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/container-mcp.js +68 -14
  61. package/dist-server/index.js +5633 -4562
  62. package/dist-server/openmausbot.js +140 -81
  63. package/dist-server/pair-cli.js +140 -81
  64. package/dist-server/server/app-installers.js +107 -7
  65. package/dist-server/server/avatar-subscription.js +51 -0
  66. package/dist-server/server/cli-help.js +74 -0
  67. package/dist-server/server/cli.js +37 -78
  68. package/dist-server/server/colima-docker.js +136 -0
  69. package/dist-server/server/container-computer.js +31 -63
  70. package/dist-server/server/container-mcp.js +5 -0
  71. package/dist-server/server/device-routing.js +60 -0
  72. package/dist-server/server/drivers/claude.js +13 -11
  73. package/dist-server/server/drivers/local-inject.js +3 -4
  74. package/dist-server/server/engine-failover.js +22 -12
  75. package/dist-server/server/engine-tiers.js +5 -14
  76. package/dist-server/server/index.js +124 -120
  77. package/dist-server/server/personal-goal-runtime.js +86 -0
  78. package/dist-server/server/personal-goal-turn.js +125 -0
  79. package/dist-server/server/personal-goals.js +195 -0
  80. package/dist-server/server/port-in-use.js +21 -0
  81. package/dist-server/server/routes/local-models.js +3 -2
  82. package/dist-server/server/session-fallback.js +76 -0
  83. package/dist-server/server/skill-fetch.js +14 -0
  84. package/dist-server/server/vault/routes.js +43 -0
  85. package/dist-server/server/vault/runtime.js +48 -0
  86. package/dist-server/server/vault/store.js +177 -0
  87. package/dist-server/shared/desktop-private-port.js +1 -0
  88. package/dist-server/shared/engine-choices.js +15 -0
  89. package/dist-server/shared/engine-tiers.js +74 -2
  90. package/dist-server/shared/local-models.js +1 -3
  91. package/dist-server/shared/personal-goal.js +43 -0
  92. package/dist-server/shared/subscription-bots.js +3 -0
  93. package/dist-server/shared/vault.js +59 -0
  94. package/dist-server/vps-container-mcp.js +4 -2
  95. package/package.json +1 -1
  96. package/dist/assets/channel-GBaPOaz-.js +0 -1
  97. package/dist/assets/index-C8YP7Zxj.css +0 -1
  98. package/dist/assets/swimlanesDiagram-TC7HE7FX-CamSa0NE.js +0 -8
@@ -1,3 +1,7 @@
1
+ import { hasUnboundGoalGuard, goalGuardForEvent, addGoalGuard, removeGoalGuard, runPersonalGoalTurn, updatePersonalGoalReceipt } from "./personal-goal-turn.js";
2
+ import { PersonalGoalRuntime, receiveGoalCommand } from "./personal-goal-runtime.js";
3
+ import { personalGoalInstructions } from "./personal-goals.js";
4
+ import { parseGoalCommand } from "../shared/personal-goal.js";
1
5
  // OpenMausBot server — the harness host. Clients hold no transports
2
6
  // (upstream rule): the React app dispatches typed commands over HTTP and
3
7
  // folds one SSE event stream; every provider process runs here.
@@ -64,7 +68,7 @@ import { turnTimingFinish, turnTimingMark, turnTimingStart } from "./turn-timing
64
68
  import { ATOMIC_TEAM_PACKAGE_ID, BUILTIN_CHIBI_REFUSALS, builtinChibiPatchRefusal } from "./builtin-chibi.js";
65
69
  import { canAccessTeam, canContactTeamOf, canManagePeer, canReachPeer, canReachTeam, coordinatorSupervises, peerAllowed, peerName, peerRosterSystemPrompt, peerStatus, peerStatusWords, reachablePeers, reachesEveryTeam, roomBoundaryHolds, teamStatusPrompt, PEER_ROSTER_MAX, resolveTeammate, roomPeerRosterSystemPrompt, rosterAbout, roomRosterLine, PEER_ACCESS_HELP } from "./peer-roster.js";
66
70
  import { openMausStatusSystemPrompt } from "./openmaus-status-capsule.js";
67
- import { containerComputerAction, managedImageNeedsUpgrade, containerComputerExists, containerComputerFrame, containerComputerMcp, containerComputerScreenshot, containerComputerStatus, recentContainerComputerStatus, containerRuntimeStatus, ensureContainerRuntime, localVmRecreatableOnDemand, localVmReplaceableWhenStopped, perBotLocalVmTarget, SHARED_LOCAL_VM_TARGET, setupCommands, } from "./container-computer.js";
71
+ import { containerComputerAction, managedImageNeedsUpgrade, containerComputerExists, containerComputerFrame, containerComputerMcp, containerComputerScreenshot, containerComputerStatus, recentContainerComputerStatus, containerRuntimeStatus, ensureContainerRuntime, containerRuntimeError, localVmRecreatableOnDemand, localVmReplaceableWhenStopped, perBotLocalVmTarget, SHARED_LOCAL_VM_TARGET, setupCommands, } from "./container-computer.js";
68
72
  import { ensureDirs, instanceConfigs, loadConfig, providerReloadKeys, localVmMaxInstances, localVmAutoStartRuntime, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, threadEventLogMaxBytes, maxConcurrentBotThreads, threadEventLogRetentionDays, saveConfig, showToolCallsEnabled, claudeUserMcpEnabled, userMcpEnabledFor, addonEnabled, skillAuthoringEnabled, sharedComputersEnabled, teamRoomsEnabled, builtInBrowserEnabled, llmThreadTitlesEnabled, onDeviceHelpersEnabled, browserProfileReplacementConflict, browserProfilePartitionTarget, syncCredentialEnv, withInstanceCli, persistableInstanceConfigs, vpsSshAlias, browserEngineAttachCdpUrl, DATA_DIR, EVENTS_DIR, NATIVE_DIR, customMcpServers, roomHandoffLimits, onConfigSaved, accountBotGrantPolicy, } from "./config.js";
69
73
  import { sweepThreadEventLogs } from "./thread-retention.js";
70
74
  import { GodModeSwitch } from "./god-mode.js";
@@ -143,7 +147,7 @@ import { MEMORY_INDEX, MemoryStoreError, memoryCapacity, memoryOverview, openFol
143
147
  import { beginMemoryTurn, endMemoryTurn, flushAllMemoryJournals, flushMemoryJournal, journalMemoryDelete, journalMemoryWrite, readMemoryJournal, revertMemoryChange, } from "./memory-journal.js";
144
148
  import { readSectionContext, readSections, sectionContextKey, sectionContextLabel, sectionContextSystemPrompt, writeSectionContext, SECTION_CONTEXT_MAX_BYTES, } from "./section-context.js";
145
149
  import { applyStagedSkillWrite, applySkillWriteWithReceipt, getStagedSkillWrite, installSkill, parseSkillMd, scanSkillText, listSkills, listStagedSkillWrites, isSkillName, readSkillFile, rejectStagedSkillWrite, removeSkill, setSkillEnabled, skillsSystemPrompt, stageSkillWrite, } from "./skills.js";
146
- import { fetchSkillFromSource } from "./skill-fetch.js";
150
+ import { resolveSkillImport, skillImportSchema } from "./skill-fetch.js";
147
151
  import { pickBotName } from "./names.js";
148
152
  import { botSkillTemplateSchema, newBotDefaultsSchema, resolveBotCreationDefaults } from "./new-bot-defaults.js";
149
153
  import { expandLearnTurnText, learnSource } from "./skill-learn.js";
@@ -235,6 +239,7 @@ import { activityCsv, activityEntries, appendAdminAction, botAuditSnapshot, botC
235
239
  import { frameForMember, memberBody, memberBot, noteSeen, notFoundFor, audienceEquals, memberGroup, narrowestAudience, parseVisibility, pathSubject, roomFeeds, routineVisible, sameAudience, SEES_EVERYTHING, SEES_ALL_BUT_PRIVATE, VisibleSet, } from "./bot-visibility.js";
236
240
  import { describeBrand, loadBrand } from "./brand.js";
237
241
  import { deliverSseFrame } from "./sse-fanout.js";
242
+ import { DeviceRouteRegistry, removeStream } from "./device-routing.js";
238
243
  import { PHONE_SECRET_PROTOCOL_VERSION, PhoneSecretBridge, PhoneSecretError, PhoneSecretSubmissionRegistry, assertPhoneSecretRequestMatches, phoneSecretOperationId, } from "./phone-secret.js";
239
244
  // Keep these two last: a route module may import any server module, and
240
245
  // loading the table after everything above leaves module start-up order as is.
@@ -248,7 +253,7 @@ import { createModelLevelsRoutes } from "./routes/model-levels.js";
248
253
  import { createMcpMarketplaceRoutes } from "./routes/mcp-marketplace.js";
249
254
  import { createLocalModelRoutes, OLLAMA_LOCAL_URL, ollamaInstalledHere } from "./routes/local-models.js";
250
255
  import { createSetupRoutes } from "./routes/setup.js";
251
- import { installOllamaApp, openPodmanInstaller, runTool, startPodmanMachine } from "./app-installers.js";
256
+ import { installOllamaApp, openPodmanInstaller, startOllamaApp, runTool, startPodmanMachine } from "./app-installers.js";
252
257
  import { CliLoginRunner } from "./cli-login.js";
253
258
  import { JobFailure, SetupJobs } from "./setup-jobs.js";
254
259
  import { localHost, probeLocalInjects } from "./drivers/local-inject.js";
@@ -258,6 +263,7 @@ import { createProviderAccountRoutes } from "./routes/provider-accounts.js";
258
263
  import { accountStops, injectedModel, restScope } from "./injected-model-rest.js";
259
264
  import { accountProvider } from "../shared/provider-accounts.js";
260
265
  import { createAccountRoutes } from "./routes/accounts.js";
266
+ import { installNativeVault } from "./vault/runtime.js";
261
267
  import { OAuthAccounts, takeOAuthClients } from "./oauth-accounts.js";
262
268
  import { DesktopAccountVault, FileAccountVault, UnavailableAccountVault, takeAccountsFromEnvironment } from "./oauth-account-vault.js";
263
269
  import { accountConnectorNames } from "./claude-account-connectors.js";
@@ -299,6 +305,7 @@ import { createBoardRoutes } from "./routes/board.js";
299
305
  import { readBoardFromNexus } from "./board/nexus-board.js";
300
306
  import { LocalModelInventory, localModePlacement, machineMemory } from "./local-models-mode.js";
301
307
  import { FailoverGate, MAX_ENGINES_PER_TURN, continuationNote, restingLine } from "./engine-failover.js";
308
+ import { enterSessionFallback, onSessionFallback, ownOrFallback, sessionFallbackSpentLine, withoutSubscription } from "./session-fallback.js";
302
309
  import { TURN_ERROR_CATEGORIES } from "../shared/runtime-events.js";
303
310
  import { EngineProbes, PROBE_PROMPT, isProbeThread, probeThreadId } from "./engine-probe.js";
304
311
  // [jev-verifier] opt-in Jev answer checks after a model switch (server/jev).
@@ -1149,9 +1156,14 @@ if (!hostedModels) {
1149
1156
  // its add-on's capability, so it loads only while the add-on applies.
1150
1157
  const bundledSkills = [...loadBundledSkills(), ...loadAddonSkillLibraries()];
1151
1158
  const availableSkills = () => mergeSkills(bundledSkills, loadUserSkills(join(DATA_DIR, "skills")));
1159
+ // Electron's utility-process parent port is private to the desktop main
1160
+ // process. It carries desktop-only messages (approval and browser control,
1161
+ // phone secrets, the account vault) without putting them in the renderer.
1162
+ // Plain Node/dev launches have no parentPort.
1152
1163
  // SAFETY: Electron's utility-process runtime is the only environment that
1153
1164
  // supplies parentPort; plain Node intentionally leaves it absent.
1154
1165
  const utilityParentPort = process.parentPort;
1166
+ installNativeVault(DATA_DIR, utilityParentPort, () => desktopMutationToken, req => authorizedInternalCapability(req.headers.authorization) !== null);
1155
1167
  function postDesktopPrivateMessage(message) {
1156
1168
  if (!utilityParentPort)
1157
1169
  return false;
@@ -1820,6 +1832,7 @@ function requestedTaskBot(botId, rawThreadId) {
1820
1832
  return task;
1821
1833
  }
1822
1834
  async function interruptDirectThread(botId, threadId) {
1835
+ personalGoals.steer(threadId);
1823
1836
  const requestOwner = directRequestOwners.get(threadId);
1824
1837
  if (requestOwner) {
1825
1838
  requestOwner.stopped = true;
@@ -3039,12 +3052,19 @@ function turnEngineChoice(botId, threadId, needs, runOn) {
3039
3052
  if (hostedModels || !profile || !task)
3040
3053
  return undefined;
3041
3054
  if (isSubscriptionBot(profile))
3042
- return subscriptionTurnEngineChoice(profile, threadId);
3055
+ return ownOrFallback(threadId, () => subscriptionTurnEngineChoice(profile, threadId), () => sessionFallbackChoice(profile, threadId, needs, runOn), (stop) => stop instanceof SubscriptionStop, (stop) => new SubscriptionStop(sessionFallbackSpentLine(stop.message)));
3043
3056
  if (isPersonalBot(profile))
3044
3057
  return personalTurnEngineChoice(profile, threadId, needs, runOn);
3045
3058
  const projected = store.projectBotForTask(botId, threadId);
3046
3059
  return projected ? ladderTurnEngineChoice(profile, projected, threadId, needs, runOn) : undefined;
3047
3060
  }
3061
+ /** A company subscription bot's conversation on another provider's subscription, same class (server/session-fallback.ts). */
3062
+ function sessionFallbackChoice(profile, threadId, needs, runOn) {
3063
+ const projected = store.projectBotForTask(profile.id, threadId);
3064
+ const choice = isPersonalBot(profile) || !projected ? undefined
3065
+ : ladderTurnEngineChoice(withoutSubscription(profile), withoutSubscription(projected), threadId, needs, runOn, true);
3066
+ return choice && choice.selection.instanceId !== projected.modelSelection.instanceId ? { selection: choice.selection, persist: false } : undefined;
3067
+ }
3048
3068
  /** A room member's engine at turn start, by turnEngineChoice's rules for
3049
3069
  * the bot's own default (a room turn has no task of its own, so the bot's
3050
3070
  * engine mode decides): resting and signed-out engines passed over, a
@@ -3060,7 +3080,7 @@ function roomTurnEngineChoice(bot, threadId) {
3060
3080
  : ladderTurnEngineChoice(bot, projected, threadId);
3061
3081
  return choice && { selection: choice.selection, persist: false };
3062
3082
  }
3063
- function ladderTurnEngineChoice(profile, projected, threadId, needs, runOn) {
3083
+ function ladderTurnEngineChoice(profile, projected, threadId, needs, runOn, subscriptionsOnly = false) {
3064
3084
  const candidates = engineSnapshots.current();
3065
3085
  if (!candidates)
3066
3086
  return undefined;
@@ -3069,7 +3089,7 @@ function ladderTurnEngineChoice(profile, projected, threadId, needs, runOn) {
3069
3089
  const { ladder } = plan;
3070
3090
  const computer = turnComputer(projected, threadId, runOn);
3071
3091
  const context = { candidates, needsDelegation: needsDelegation(tier, profile), computer, refuse: engineTierRefusal(projected), resting: engineResting,
3072
- keyBandPaused: payPerUse.pausedUntil() !== undefined };
3092
+ keyBandPaused: subscriptionsOnly || payPerUse.pausedUntil() !== undefined };
3073
3093
  const local = (resolution) => localModeTurn(resolution, { tier, context, needs, prefer: [projected.modelSelection.instanceId] });
3074
3094
  const wider = (tried, modelClass) => resolveFailover({
3075
3095
  ladder, classLadder: (cls) => classLadder(classedModels(), cls), failed: projected.modelSelection, context, tried,
@@ -3350,7 +3370,7 @@ function localModeTurn(resolution, turn) {
3350
3370
  return { ...resolution, local: true, choice: { ...choice, selection: placement.selection, candidate: placement.candidate } };
3351
3371
  }
3352
3372
  const payPerUsePausedLine = (until) => `Pay-per-use is paused until ${restTime(until)}, when a subscription is back, so this turn stopped.`;
3353
- /** A model this machine serves: never a stand-in for Engineering and above. */
3373
+ /** A model this machine serves: a rung when a ladder names it, else local rules only. */
3354
3374
  const isLocalModel = (candidate, model) => localModel(candidate, model) || modelLevels.classOf(candidate.driverKind, model)?.local === true;
3355
3375
  // ── silent automatic failover (docs/chibitek/engine-tiers.md, step 2) ────
3356
3376
  // Resting engines are shared by every bot and survive a restart.
@@ -3589,10 +3609,7 @@ const eventBase = (event) => ({
3589
3609
  ...(event.providerInstanceId ? { providerInstanceId: event.providerInstanceId } : {}), ...(event.turnId ? { turnId: event.turnId } : {}),
3590
3610
  });
3591
3611
  const restTime = (at) => new Date(at).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
3592
- /** A watched turn ended failed because its engine ran out or was signed
3593
- * out. Rest the engine, then run the same turn on the next engine at the
3594
- * same level, silently; or, when nothing is left, stop it with one line
3595
- * naming the resting engines. Returns what subscribers see. */
3612
+ /** Rest a refused engine and continue on an eligible engine, or show an actionable line. */
3596
3613
  function engineFailover(turn, completion) {
3597
3614
  const failure = turn.failure;
3598
3615
  const record = failoverTurns.get(turn.threadId);
@@ -3615,17 +3632,14 @@ function engineFailover(turn, completion) {
3615
3632
  const restFailed = () => engineRests.rest(turn.instanceId, failedCandidate ? engineAccount(failedCandidate) : undefined, failure.kind, { resetAt: failure.resetAt, resetText: failure.resetText, windowMs: failure.windowMs, message: redactSecretsInText(failure.message),
3616
3633
  ...(failure.permanent ? { permanent: true } : {}), ...(scope ? { scope } : {}), ...(ran?.model ? { model: ran.model } : {}),
3617
3634
  ...(failure.kind === "auth_failed" && failedCandidate ? { reportedSignedIn: failedCandidate.snapshot.state === "available" && failedCandidate.snapshot.authenticated === true } : {}) });
3618
- // An unrecognised failure (provider_error) is only a reason to try the
3619
- // next engine: the engine rests (the circuit breaker) only once the turn
3620
- // really moves, and with nowhere to go the turn ends with the engine's own
3621
- // report, exactly as before, rather than a "resting" line about it.
3635
+ // Unknown failures rest only when the turn moves; otherwise preserve their report.
3622
3636
  const rest = failure.kind === "provider_error" ? undefined : restFailed();
3623
3637
  const label = (instanceId) => candidates.find((candidate) => candidate.instanceId === instanceId)?.displayName ?? instanceId;
3624
3638
  // The one plain line, never the provider's own words: which account needs
3625
3639
  // the person and where to fix it, or that every engine is resting.
3626
3640
  const stop = (line, category = "failover_exhausted") => [
3627
3641
  { ...eventBase(completion), type: "runtime.error", message: line, terminal: true, category, ...(failure.setup ? { setup: true } : {}) },
3628
- completion,
3642
+ failure.kind === "auth_failed" ? { ...completion, stopReason: "auth_required" } : completion,
3629
3643
  ];
3630
3644
  const restingNamed = (instanceIds) => [...new Set(instanceIds)].flatMap((instanceId) => {
3631
3645
  const candidate = candidates.find((entry) => entry.instanceId === instanceId);
@@ -3648,13 +3662,11 @@ function engineFailover(turn, completion) {
3648
3662
  return stop(pick.line, pick.category);
3649
3663
  const { choice } = pick;
3650
3664
  const tried = new Set(record.tried);
3651
- // Move the turn. The attempt that failed is retired quietly afterwards: no
3652
- // chip, no notification, no incident, and nothing settled that the same
3653
- // turn still owns.
3665
+ // Retire the failed attempt without settling the request that the next engine owns.
3654
3666
  const moved = ran ?? record.selection;
3655
3667
  const failedOwner = turnResourceOwners.get(turn.threadId);
3656
3668
  const tokens = completion.usage ?? turnUsage.get(turn.threadId);
3657
- const partialText = turn.partial.trim();
3669
+ const partialText = groupGoalCoordinatorTurns.get(turn.threadId)?.size ? parseGroupGoalDecision(turn.partial).visibleText : turn.partial.trim();
3658
3670
  const requestOwner = directRequestOwners.get(turn.threadId);
3659
3671
  if (partialText) {
3660
3672
  // The answer it was in the middle of stays: the person saw it stream.
@@ -3666,6 +3678,12 @@ function engineFailover(turn, completion) {
3666
3678
  const tools = earlier.filter((message) => message.kind === "activity" && message.tool?.name).map((message) => message.tool.name);
3667
3679
  const note = continuationNote(said.map((message) => message.text ?? ""), tools);
3668
3680
  const attempt = record.attempt + 1;
3681
+ for (const guard of groupGoalCoordinatorTurns.get(turn.threadId) ?? []) {
3682
+ if (!guard.discard && (guard.turnId === completion.turnId || guard.turnId === turn.turnId)) {
3683
+ guard.turnId = undefined;
3684
+ guard.assistantItems = [];
3685
+ }
3686
+ }
3669
3687
  const rerun = startTurn(turn.botId, note ? `${record.text}\n\n${note}` : record.text, {
3670
3688
  ...record.opts,
3671
3689
  threadId: turn.threadId,
@@ -3703,6 +3721,11 @@ function engineFailover(turn, completion) {
3703
3721
  });
3704
3722
  }
3705
3723
  retireFailedAttempt(turn.botId, turn.threadId, turn.generation, failedOwner, completion);
3724
+ if (failure.kind === "auth_failed")
3725
+ reportIncident({
3726
+ kind: "engine-fallback", bot: profile, threadId: turn.threadId, notifyChief: true,
3727
+ detail: `${engineLabel(candidates, moved)} needs signing in again in Settings > Engines; the turn continued on ${engineLabel(candidates, choice.selection)}`,
3728
+ });
3706
3729
  console.error(`[omb-failover] thread=${turn.threadId} ${failure.kind}: ${moved.instanceId} -> ${choice.selection.instanceId} (attempt ${attempt})`);
3707
3730
  // No turnId: the failed provider turn is retired, and its id would hide
3708
3731
  // this record from every window.
@@ -3803,14 +3826,19 @@ function failoverPick(turn, profile, projected, candidates, failedCandidate, res
3803
3826
  const computer = projected ? turnComputer(projected, turn.threadId, attempt.runOn) : undefined;
3804
3827
  // A subscription bot fails over only to another account of its own
3805
3828
  // provider on its side of the personal line; with none left it stops with
3806
- // its one line (and the client offers Chibi). Never another provider.
3829
+ // its one line, or a company bot's own conversation takes its session fallback (server/session-fallback.ts).
3807
3830
  const subscriptionMove = isSubscriptionBot(profile)
3808
3831
  ? tried.size >= MAX_ENGINES_PER_TURN || !projected
3809
3832
  ? { line: subscriptionLine(profile, candidates) }
3810
3833
  : subscriptionPlacement({ profile, projected, candidates, tried: new Set([...tried, turn.instanceId]), auto: attempt.persist, modelClass: neededClass })
3811
3834
  : undefined;
3812
- if (subscriptionMove && !subscriptionMove.choice)
3835
+ const fallback = subscriptionMove && !subscriptionMove.choice && projected && !isPersonalBot(profile) && tried.size < MAX_ENGINES_PER_TURN && store.taskByThread(profile.id, turn.threadId);
3836
+ if (fallback && !enterSessionFallback(turn.threadId, false))
3837
+ return { line: sessionFallbackSpentLine(subscriptionMove.line) };
3838
+ if (subscriptionMove && !subscriptionMove.choice && !fallback)
3813
3839
  return { line: subscriptionMove.line };
3840
+ if (fallback)
3841
+ [profile, projected] = [withoutSubscription(profile), withoutSubscription(projected)];
3814
3842
  const personalTurn = isPersonalBot(profile) && !subscriptionMove;
3815
3843
  // The Private Bot prefers personal engines and opted-in local compute,
3816
3844
  // then company compute. startTurn rechecks company cost on every attempt.
@@ -3830,7 +3858,7 @@ function failoverPick(turn, profile, projected, candidates, failedCandidate, res
3830
3858
  classLadder: (modelClass) => classLadder(classedModels(), modelClass),
3831
3859
  failed: attempt.selection,
3832
3860
  context: { candidates, needsDelegation: needsDelegation(tier, profile), computer, refuse: engineTierRefusal(projected), resting: engineResting,
3833
- keyBandPaused: keyBandPausedFor(turn.threadId) },
3861
+ keyBandPaused: Boolean(fallback) || keyBandPausedFor(turn.threadId) },
3834
3862
  tried,
3835
3863
  modelClass: neededClass,
3836
3864
  mayDrop: true,
@@ -3846,6 +3874,8 @@ function failoverPick(turn, profile, projected, candidates, failedCandidate, res
3846
3874
  context: { candidates, needsDelegation: needsDelegation(tier, profile), computer, refuse: engineTierRefusal(projected), resting: engineResting } })
3847
3875
  : { ...resolution, local: false };
3848
3876
  const choice = placed.choice && placed.local ? { ...placed.choice, modelClass: "Añejo" } : placed.choice;
3877
+ if (!choice && fallback)
3878
+ return { line: subscriptionMove.line };
3849
3879
  if (!choice && turn.failure?.kind === "provider_error")
3850
3880
  return { passthrough: true };
3851
3881
  if (!choice) {
@@ -3855,11 +3885,11 @@ function failoverPick(turn, profile, projected, candidates, failedCandidate, res
3855
3885
  const resting = resolution.skipped.filter((skip) => skip.reason === "resting" || skip.reason === "tried").map((skip) => skip.instanceId);
3856
3886
  const named = restingNamed([turn.instanceId, ...attempt.tried, ...resting]);
3857
3887
  return {
3858
- line: restingLine(named, restTime, cannotDriveNamed(candidates, resolution.skipped.map((skip) => skip.instanceId), computer)),
3888
+ line: restingLine(named, restTime, cannotDriveNamed(candidates, resolution.skipped.map((skip) => skip.instanceId), computer), { capped: tried.size >= MAX_ENGINES_PER_TURN }),
3859
3889
  category: named.some((engine) => engine.needsSignIn || engine.keyExpired) ? "account" : "failover_exhausted",
3860
3890
  };
3861
3891
  }
3862
- if (subscriptionMove) { /* subscriptions only: no pay-per-use episode */ }
3892
+ if (subscriptionMove || fallback) { /* subscriptions only: no pay-per-use episode */ }
3863
3893
  else if (personalMove)
3864
3894
  notePersonalPayPerUse(personalMove);
3865
3895
  else
@@ -5013,67 +5043,21 @@ const publicBotQueuedMessages = () => queuedSteerSnapshot((botId, threadId) => B
5013
5043
  const groupTurnOperations = new Map();
5014
5044
  const groupGoalCoordinatorTurns = new Map();
5015
5045
  const GROUP_GOAL_COORDINATOR_GUARD_MS = 5 * 60_000;
5016
- function addGroupGoalCoordinatorTurn(threadId, turn) {
5017
- const turns = groupGoalCoordinatorTurns.get(threadId) ?? new Set();
5018
- turns.add(turn);
5019
- groupGoalCoordinatorTurns.set(threadId, turns);
5020
- }
5021
- function removeGroupGoalCoordinatorTurn(threadId, turn) {
5022
- if (turn.cleanupTimer)
5023
- clearTimeout(turn.cleanupTimer);
5024
- const turns = groupGoalCoordinatorTurns.get(threadId);
5025
- turns?.delete(turn);
5026
- if (turns?.size === 0)
5027
- groupGoalCoordinatorTurns.delete(threadId);
5028
- }
5029
- function hasUnboundDiscardedGroupGoalTurn(threadId) {
5030
- return [...(groupGoalCoordinatorTurns.get(threadId) ?? [])]
5031
- .some((turn) => turn.discard && !turn.turnId);
5032
- }
5033
- /** Match private coordinator output to one provider turn, never merely to a
5034
- * reusable room thread. Most adapters emit turn.started before sendTurn
5035
- * resolves, so the first stable event may bind an otherwise pending guard. */
5036
- function groupGoalCoordinatorTurnForEvent(event) {
5037
- const turns = groupGoalCoordinatorTurns.get(event.threadId);
5038
- if (!turns?.size)
5039
- return undefined;
5040
- const candidates = [...turns];
5041
- if (event.turnId) {
5042
- const exact = candidates.find((turn) => turn.turnId === event.turnId);
5043
- if (exact)
5044
- return exact;
5045
- // Until an interrupted handshake returns its own id, no new id can be
5046
- // attributed safely. The stall fallback keeps this thread unavailable in
5047
- // that narrow window; private text is suppressed below until sendTurn's
5048
- // result binds the old guard or its bounded expiry releases ownership.
5049
- const unboundDiscarded = candidates.filter((turn) => turn.discard && !turn.turnId);
5050
- if (unboundDiscarded.length > 0) {
5051
- // The ownership fallback below keeps a lone abandoned handshake's
5052
- // thread closed, so its first eventual id can safely bind here. More
5053
- // than one unbound candidate is genuinely ambiguous and stays gated.
5054
- if (candidates.length === 1) {
5055
- unboundDiscarded[0].turnId = event.turnId;
5056
- // This event is the first stable identity for an already-abandoned
5057
- // provider turn. Tombstone it immediately so this event and every
5058
- // later completion/request cannot settle a replacement on the same
5059
- // room thread.
5060
- retireProviderTurn(event.turnId);
5061
- return unboundDiscarded[0];
5062
- }
5063
- return undefined;
5064
- }
5065
- const pending = candidates.findLast((turn) => !turn.turnId && !turn.discard);
5066
- if (pending && !pending.turnId) {
5067
- pending.turnId = event.turnId;
5068
- return pending;
5069
- }
5070
- return undefined;
5071
- }
5072
- // Turn-scoped events normally carry an id. If an adapter omits it, fail
5073
- // closed for private text; with multiple overlapping guards there is no
5074
- // safe way to attribute a completion, so leave cleanup to the bounded timer.
5075
- return candidates.length === 1 ? candidates[0] : undefined;
5076
- }
5046
+ const personalGoals = new PersonalGoalRuntime(join(DATA_DIR, "personal-goals.json"), {
5047
+ busy: (botId, threadId) => threadBusy(botId, threadId) || Boolean(activeGroupTurnForBot(botId)),
5048
+ interrupt: async (botId, threadId, goalId) => {
5049
+ const owns = [...(groupGoalCoordinatorTurns.get(threadId) ?? [])].some(guard => guard.personalGoalId === goalId && !guard.discard && (!guard.turnId || guard.turnId === liveTurnByThread.get(threadId)));
5050
+ if (owns)
5051
+ await interruptDirectThread(botId, threadId);
5052
+ },
5053
+ changed: (goal) => updatePersonalGoalReceipt(goal, store),
5054
+ run: (goal) => runPersonalGoalTurn(goal, { store, addGuard: addGroupGoalCoordinatorTurn, removeGuard: removeGroupGoalCoordinatorTurn,
5055
+ subscribe: (listener) => bus.subscribe(listener), ignore: shouldIgnoreProviderEvent, busy: threadBusy, start: startTurn }),
5056
+ });
5057
+ const addGroupGoalCoordinatorTurn = (threadId, turn) => addGoalGuard(groupGoalCoordinatorTurns, threadId, turn);
5058
+ const removeGroupGoalCoordinatorTurn = (threadId, turn) => removeGoalGuard(groupGoalCoordinatorTurns, threadId, turn);
5059
+ const hasUnboundDiscardedGroupGoalTurn = (threadId) => hasUnboundGoalGuard(groupGoalCoordinatorTurns, threadId);
5060
+ const groupGoalCoordinatorTurnForEvent = (event) => goalGuardForEvent(groupGoalCoordinatorTurns, retireProviderTurn, event);
5077
5061
  function groupIsWorking(group) {
5078
5062
  return Boolean(group.busyBotId) || Boolean(groupTurnOperations.get(group.id)?.size);
5079
5063
  }
@@ -6024,6 +6008,9 @@ function messageWindow(threadId, messageId, limit) {
6024
6008
  return { messages: all.slice(start, stop).map(slimMessage), hasMore: start > 0 };
6025
6009
  }
6026
6010
  const sseClients = new Set();
6011
+ // Approval device routing: docs/specs/remote-approval/spec.md.
6012
+ const deviceRoutes = new DeviceRouteRegistry();
6013
+ const removeSseClient = (client) => removeStream(sseClients, deviceRoutes, client);
6027
6014
  /** The frame at which a bot's audience last changed (see the SSE route). */
6028
6015
  let audienceChangedAt = 0;
6029
6016
  /** A bot's audience changed: end every member stream. Each reconnects at
@@ -6035,27 +6022,15 @@ function audienceChanged() {
6035
6022
  for (const client of sseClients) {
6036
6023
  if (client.viewer.kind !== "member")
6037
6024
  continue;
6038
- sseClients.delete(client);
6039
- try {
6040
- client.res.end();
6041
- }
6042
- catch {
6043
- /* already gone */
6044
- }
6025
+ removeSseClient(client);
6045
6026
  }
6046
6027
  }
6047
6028
  function closeSessionStreams(sessionId) {
6048
6029
  browserLive.closeForOwner(sessionId);
6049
- for (const client of sseClients) {
6030
+ for (const client of Array.from(sseClients)) {
6050
6031
  if (client.sessionId !== sessionId)
6051
6032
  continue;
6052
- sseClients.delete(client);
6053
- try {
6054
- client.res.end();
6055
- }
6056
- catch {
6057
- /* already gone */
6058
- }
6033
+ removeSseClient(client);
6059
6034
  }
6060
6035
  }
6061
6036
  sessions.onSessionRevoked((sessionId) => {
@@ -6253,7 +6228,7 @@ function broadcast(payload, options = {}) {
6253
6228
  // Screen frames are replaceable and durable events are not: see
6254
6229
  // ./sse-fanout.ts for the backpressure/bound decision this makes.
6255
6230
  if (deliverSseFrame(client, kind, out) === "disconnected") {
6256
- sseClients.delete(client);
6231
+ removeSseClient(client);
6257
6232
  }
6258
6233
  }
6259
6234
  }
@@ -7019,15 +6994,15 @@ function localVmSetUp() {
7019
6994
  }
7020
6995
  /** The one gate for starting the container engine (App Settings →
7021
6996
  * Computers): the person's switch, the organisation's policy, and a Local VM
7022
- * actually set up here. Returns what it started ("Colima", "Docker
7023
- * Desktop") once the engine answers, or null. */
6997
+ * actually set up here. Returns what it started (Colima, the only engine
6998
+ * Atomic starts; never Docker Desktop) once it answers, or null. */
7024
6999
  async function startLocalVmRuntime() {
7025
7000
  if (!localVmAutoStartRuntime(cfg) || !managedPolicy.computerAllowed("localVm") || !localVmSetUp())
7026
7001
  return null;
7027
7002
  try {
7028
- const { status, started } = await ensureContainerRuntime();
7029
- if (started)
7030
- console.log(`[local-vm] started ${started}: ${status.daemonUp ? "ready" : "still not answering"}`);
7003
+ const { status, started, error } = await ensureContainerRuntime();
7004
+ if (started || error)
7005
+ console[error ? "error" : "log"](`[local-vm] ${started ? `started ${started}: ${status.daemonUp ? "ready" : "still not answering"}` : "not started"}${error ? `. ${error}` : ""}`);
7031
7006
  return started && status.daemonUp ? started : null;
7032
7007
  }
7033
7008
  catch (error) {
@@ -7539,7 +7514,7 @@ async function unusableAutoPin(bot, threadId, plan, runOn) {
7539
7514
  if (pinned === "vm" && !choice.available && !choice.starting && await startLocalVmRuntime()) {
7540
7515
  choice = await computerChoice(bot, pinned);
7541
7516
  }
7542
- return choice.available || choice.starting ? null : choice.reason ?? `${surfaceLabel(pinned)} is unavailable`;
7517
+ return choice.available || choice.starting ? null : (pinned === "vm" ? containerRuntimeError() : null) ?? choice.reason ?? `${surfaceLabel(pinned)} is unavailable`;
7543
7518
  }
7544
7519
  function continueComputerSelection(threadId, generation, succeeded) {
7545
7520
  const selection = computerSelectionTurns.get(threadId);
@@ -8271,7 +8246,7 @@ bus.subscribe((event) => {
8271
8246
  if (!event.ok && event.stopReason !== "interrupted" && !lazyClaimAlreadyReported && !routines?.runForThread(event.threadId)) {
8272
8247
  const broken = bot ?? (speaker ? store.bot(speaker.botId) : undefined);
8273
8248
  if (broken)
8274
- reportIncident({ kind: "failed", bot: broken, threadId: event.threadId, detail: failedRunDetail(event.stopReason, turnError) });
8249
+ reportIncident({ kind: "failed", bot: broken, threadId: event.threadId, detail: failedRunDetail(event.stopReason, turnError), notifyChief: event.stopReason === "auth_required" });
8275
8250
  }
8276
8251
  const lastReported = turnUsage.get(event.threadId);
8277
8252
  turnUsage.delete(event.threadId);
@@ -8637,7 +8612,7 @@ function reportIncident(input) {
8637
8612
  const task = store.taskByThread(bot.id, threadId);
8638
8613
  // A thread another bot opened and is watching is that bot's to handle:
8639
8614
  // the delegator is woken with the failure already (wakeDelegationSource).
8640
- if (task?.openedBy?.delegationId || delegationWatch.has(threadId) || roomHandoffs.activeDirect(threadId))
8615
+ if (!input.notifyChief && (task?.openedBy?.delegationId || delegationWatch.has(threadId) || roomHandoffs.activeDirect(threadId)))
8641
8616
  return;
8642
8617
  const group = store.groupByThread(threadId);
8643
8618
  const incident = {
@@ -9678,7 +9653,8 @@ async function startTurn(botId, text, opts) {
9678
9653
  : threadBusy(botId, threadId) ? undefined : turnEngineChoice(botId, threadId, { tools: true, vision: turnHasImages(opts?.userMessage) }, opts?.runOn);
9679
9654
  const tierSelection = engineChoice?.persist ? engineChoice.selection : undefined;
9680
9655
  const projectedBot = store.projectBotForTask(botId, threadId);
9681
- const bot = projectedBot && engineChoice ? { ...projectedBot, modelSelection: engineChoice.selection } : projectedBot;
9656
+ const fallbackBot = projectedBot && !opts?.engine && onSessionFallback(threadId) ? withoutSubscription(projectedBot) : projectedBot;
9657
+ const bot = fallbackBot && engineChoice ? { ...fallbackBot, modelSelection: engineChoice.selection } : fallbackBot;
9682
9658
  if (!bot)
9683
9659
  throw Object.assign(new Error("no such task"), { status: 404 });
9684
9660
  // Routines and legacy peer delivery already have their own completion
@@ -10760,6 +10736,7 @@ async function startTurn(botId, text, opts) {
10760
10736
  ? await nexusMemory.readBack({ bot: liveBot ?? bot, threadId, query: providerText })
10761
10737
  : "";
10762
10738
  const prompt = buildSystemPrompt(persona, liveBot?.soul ?? bot.soul ?? "", roomPromptSections(roomTurn, [
10739
+ { id: "personal-goal", label: "Active goal", text: personalGoals.goals.get(threadId)?.status === "working" ? personalGoalInstructions(personalGoals.goals.get(threadId)) : "" },
10763
10740
  { id: "user-profile", label: "About the user", text: userProfileSystemPrompt(cfg.profile) },
10764
10741
  // the ICM folder is on this machine; a cloud run cannot reach it
10765
10742
  // first after the soul: the block names agent tools, so it only goes
@@ -14356,6 +14333,8 @@ async function readyLocalVmForTurn(botId, target, isCurrent = () => true, onNote
14356
14333
  onNote?.(`Started ${started} for the Local VM`);
14357
14334
  status = await containerComputerStatus(undefined, undefined, target);
14358
14335
  }
14336
+ else if (containerRuntimeError() && isCurrent())
14337
+ onNote?.(containerRuntimeError());
14359
14338
  }
14360
14339
  noteLocalVmSeen(target, status);
14361
14340
  if (!isCurrent())
@@ -16129,7 +16108,7 @@ ROUTES.push(createSetupRoutes({
16129
16108
  }
16130
16109
  },
16131
16110
  startOllama: async (context) => {
16132
- await runTool("/usr/bin/open", ["-a", "Ollama"]);
16111
+ startOllamaApp();
16133
16112
  for (let attempt = 0; attempt < 30 && !context.signal.aborted; attempt++) {
16134
16113
  if ((await setupOllamaState()) === "running")
16135
16114
  return;
@@ -19062,6 +19041,7 @@ const handleRequest = async (req, res) => {
19062
19041
  admin: auth.scopes.includes("admin"),
19063
19042
  screens: url.searchParams.get("screens") !== "off",
19064
19043
  backpressured: false,
19044
+ authKind: auth.kind,
19065
19045
  viewer,
19066
19046
  // What this person's hydration (GET /api/bots, narrowed the same way) showed them.
19067
19047
  seen: viewer.kind === "all" && viewer.privateOwner ? { bots: new Set(), groups: new Set() } : {
@@ -19105,6 +19085,7 @@ const handleRequest = async (req, res) => {
19105
19085
  // A client that offered no cursor gets false too, which is exactly
19106
19086
  // what a cold start should do.
19107
19087
  resumed,
19088
+ approvalTarget: deviceRoutes.route(),
19108
19089
  })}\n\n`);
19109
19090
  if (resumed) {
19110
19091
  for (const buffered of replayBuffer) {
@@ -19120,6 +19101,7 @@ const handleRequest = async (req, res) => {
19120
19101
  }
19121
19102
  }
19122
19103
  sseClients.add(client);
19104
+ deviceRoutes.streamOpened(client.authKind);
19123
19105
  // Keep this long-lived response out of socket idle-timeout handling
19124
19106
  // without weakening timeouts for every other API request.
19125
19107
  req.socket.setTimeout(0);
@@ -19139,7 +19121,7 @@ const handleRequest = async (req, res) => {
19139
19121
  }, SSE_HEARTBEAT_MS);
19140
19122
  req.on("close", () => {
19141
19123
  clearInterval(keepalive);
19142
- sseClients.delete(client);
19124
+ removeSseClient(client);
19143
19125
  });
19144
19126
  return;
19145
19127
  }
@@ -20584,11 +20566,11 @@ const handleRequest = async (req, res) => {
20584
20566
  });
20585
20567
  }
20586
20568
  if (method === "POST" && path === "/api/bot-defaults/skills/preview") {
20587
- const checked = z.object({ source: z.string().min(1).max(2000) }).strict().safeParse(await readBody(req));
20569
+ // Drafts accept the same uploaded bytes as existing bots, without installing them.
20570
+ const checked = skillImportSchema.safeParse(await readBody(req));
20588
20571
  if (!checked.success)
20589
20572
  return json(res, 400, { error: checked.error.message });
20590
- const input = checked.data;
20591
- const fetched = await fetchSkillFromSource(input.source);
20573
+ const fetched = await resolveSkillImport(checked.data);
20592
20574
  if ("error" in fetched)
20593
20575
  return json(res, 422, { error: fetched.error });
20594
20576
  const skills = [];
@@ -21701,10 +21683,10 @@ const handleRequest = async (req, res) => {
21701
21683
  if (m && method === "POST") {
21702
21684
  if (!store.bot(m[1]))
21703
21685
  return json(res, 404, { error: "no such bot" });
21704
- const parsed = z.object({ source: z.string().min(1).max(2000) }).safeParse(await readBody(req));
21686
+ const parsed = skillImportSchema.safeParse(await readBody(req));
21705
21687
  if (!parsed.success)
21706
- return json(res, 400, { error: "source must be a GitHub URL or owner/repo" });
21707
- const fetched = await fetchSkillFromSource(parsed.data.source);
21688
+ return json(res, 400, { error: "Choose a Markdown skill file or enter a GitHub source (maximum 256 KiB per file)." });
21689
+ const fetched = await resolveSkillImport(parsed.data);
21708
21690
  if ("error" in fetched)
21709
21691
  return json(res, 422, { error: fetched.error });
21710
21692
  const results = fetched.skills.map((skill) => installSkill(m[1], skill.source, skill.files));
@@ -22233,10 +22215,20 @@ const handleRequest = async (req, res) => {
22233
22215
  userAgent: typeof req.headers["user-agent"] === "string" ? req.headers["user-agent"] : undefined,
22234
22216
  relayed: Boolean(onBehalfOf),
22235
22217
  }));
22218
+ let goalCommand;
22219
+ try {
22220
+ goalCommand = parseGoalCommand(text);
22221
+ }
22222
+ catch (error) {
22223
+ return json(res, 400, { error: error instanceof Error ? error.message : "Invalid goal command" });
22224
+ }
22225
+ if (guarded && goalCommand)
22226
+ return json(res, 400, { error: "Goal commands require the conversation command interface." });
22236
22227
  // The send is acknowledged before the turn starts, so a workspace at its
22237
22228
  // spend limit is refused here, where the person can see it.
22238
22229
  try {
22239
- assertWithinBudget(cfg, DATA_DIR);
22230
+ if (!goalCommand || goalCommand.action === "start" || goalCommand.action === "resume")
22231
+ assertWithinBudget(cfg, DATA_DIR);
22240
22232
  }
22241
22233
  catch (error) {
22242
22234
  return json(res, 409, { error: error instanceof Error ? error.message : String(error), code: "spend_cap" });
@@ -22305,6 +22297,12 @@ const handleRequest = async (req, res) => {
22305
22297
  const message = await startTurn(bot.id, text, { threadId, replyTo, sendId, sender: messageSender(auth), trigger });
22306
22298
  return { ok: true, threadId, message };
22307
22299
  }
22300
+ if (goalCommand) {
22301
+ if (auth.kind === "assistant")
22302
+ throw Object.assign(new Error("Only the person controls goals."), { status: 403 });
22303
+ return receiveGoalCommand(personalGoals, store, bot.id, threadId, goalCommand, text, sendId, messageSender(auth), trigger);
22304
+ }
22305
+ personalGoals.steer(threadId);
22308
22306
  // Claude can accept the message inside its live turn. If the write
22309
22307
  // loses a race with turn settlement, or the engine cannot steer, the
22310
22308
  // existing server-side queue records it atomically for the next turn.
@@ -23733,6 +23731,8 @@ const handleRequest = async (req, res) => {
23733
23731
  if (method === "GET" && authStatus) {
23734
23732
  res.setHeader("cache-control", "no-store");
23735
23733
  const state = await providerAuthSessions.status(authStatus[1], auth.kind === "session" ? auth.session.id : "loopback", url.searchParams.get("flowId") ?? "");
23734
+ if (state.phase === "succeeded")
23735
+ engineRests.clear(authStatus[1], "signed-in");
23736
23736
  return json(res, 200, { auth: state });
23737
23737
  }
23738
23738
  const instanceAction = /^\/api\/instances\/([\w.-]+)\/(refresh-models|install|auth\/start|auth\/complete|auth\/cancel|auth\/sign-out)$/.exec(path);
@@ -23774,6 +23774,8 @@ const handleRequest = async (req, res) => {
23774
23774
  providerAuthSessions.revokeOwner(owner);
23775
23775
  return json(res, 401, { error: "Your session ended. Start a new sign-in." });
23776
23776
  }
23777
+ if (started.phase === "succeeded")
23778
+ engineRests.clear(instanceId, "signed-in");
23777
23779
  return json(res, 200, { auth: started });
23778
23780
  }
23779
23781
  if (action === "auth/sign-out") {
@@ -23791,6 +23793,8 @@ const handleRequest = async (req, res) => {
23791
23793
  if (!flowId || !callbackUrl)
23792
23794
  return json(res, 400, { error: "flowId and a code or callbackUrl are required" });
23793
23795
  await providerAuthSessions.complete(instanceId, owner, flowId, callbackUrl);
23796
+ engineRests.clear(instanceId, "signed-in");
23797
+ await describeEngines();
23794
23798
  return json(res, 200, { ok: true });
23795
23799
  }
23796
23800
  const body = await readBody(req, 4096);