mixdog 1.3.3 → 1.3.5

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 (210) hide show
  1. package/LICENSES/oh-my-pi-MIT.txt +23 -0
  2. package/NOTICE.md +15 -0
  3. package/README.md +11 -0
  4. package/package.json +3 -2
  5. package/scripts/embed-google-oauth-client.mjs +49 -0
  6. package/scripts/lib/test-spawn-ledger.mjs +46 -1
  7. package/scripts/test.mjs +5 -0
  8. package/src/defaults/shipped-history.json +8 -0
  9. package/src/defaults/skills/browser-use/SKILL.md +21 -9
  10. package/src/defaults/skills/setup/SKILL.md +7 -2
  11. package/src/defaults/skills/setup/references/actions.md +82 -27
  12. package/src/defaults/skills/setup/references/surfaces.md +67 -34
  13. package/src/defaults/skills/setup-guide/SKILL.md +3 -3
  14. package/src/lib/keychain-cjs.cjs +359 -29
  15. package/src/rules/agent-mode/AGENT_MODE.md +10 -2
  16. package/src/rules/agent-mode/MIXY.md +12 -0
  17. package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
  18. package/src/runtime/agent/orchestrator/config.mjs +18 -0
  19. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +2 -2
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
  21. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
  22. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
  23. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
  24. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
  25. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
  26. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
  27. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +68 -23
  28. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +22 -1
  29. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
  30. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -3
  32. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
  33. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
  34. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
  35. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
  36. package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
  37. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
  41. package/src/runtime/agent/orchestrator/providers/cursor.mjs +7 -0
  42. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
  43. package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
  44. package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
  45. package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
  46. package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
  47. package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
  48. package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
  49. package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
  50. package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
  51. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
  52. package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
  53. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
  54. package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
  55. package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
  56. package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
  57. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
  58. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
  60. package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
  61. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
  63. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
  64. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +17 -0
  65. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
  66. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  67. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
  69. package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
  70. package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
  71. package/src/runtime/agent/orchestrator/providers/registry.mjs +17 -3
  72. package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
  73. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
  75. package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
  76. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  77. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
  78. package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
  79. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
  80. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
  81. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
  82. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
  83. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
  84. package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
  85. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +15 -3
  86. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -0
  87. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +36 -4
  88. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -0
  89. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +15 -3
  90. package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
  91. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
  92. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
  93. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +50 -2
  94. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +11 -2
  96. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +11 -2
  97. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
  98. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
  99. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
  100. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
  101. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
  102. package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
  103. package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
  104. package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
  105. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
  106. package/src/runtime/agent/orchestrator/session/routine-result.mjs +11 -2
  107. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
  108. package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
  109. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +125 -12
  110. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
  111. package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
  112. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
  113. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +7 -1
  114. package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
  115. package/src/runtime/channels/lib/scheduler.mjs +109 -14
  116. package/src/runtime/channels/lib/schedules-db.mjs +49 -0
  117. package/src/runtime/channels/lib/tool-dispatch.mjs +13 -0
  118. package/src/runtime/channels/lib/worker-main.mjs +9 -0
  119. package/src/runtime/office/com/com-adapter.mjs +17 -1
  120. package/src/runtime/office/com/office-com-host.ps1 +51 -1
  121. package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
  122. package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
  123. package/src/runtime/office/pdf/document-preview.mjs +326 -8
  124. package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
  125. package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
  126. package/src/runtime/office/portable/portable-soffice.mjs +45 -5
  127. package/src/runtime/shared/agent-room-host.mjs +1 -1
  128. package/src/runtime/shared/bot-hops.mjs +62 -2
  129. package/src/runtime/shared/config.mjs +49 -22
  130. package/src/runtime/shared/connector-catalog.d.mts +4 -2
  131. package/src/runtime/shared/connector-catalog.mjs +16 -8
  132. package/src/runtime/shared/developer-options.mjs +32 -1
  133. package/src/runtime/shared/google-oauth-client.generated.json +4 -0
  134. package/src/runtime/shared/google-oauth-client.mjs +34 -9
  135. package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
  136. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
  137. package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
  138. package/src/runtime/shared/llm/usage-rollup.mjs +4 -383
  139. package/src/runtime/shared/llm/well-formed.mjs +66 -0
  140. package/src/runtime/shared/provider-accounts.mjs +224 -58
  141. package/src/runtime/shared/provider-api-key.mjs +19 -8
  142. package/src/runtime/shared/routine-trigger.mjs +42 -0
  143. package/src/runtime/shared/tool-execution-contract.mjs +2 -0
  144. package/src/runtime/shared/transcript-metadata.mjs +38 -0
  145. package/src/runtime/shared/user-data-guard.mjs +30 -2
  146. package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
  147. package/src/session-runtime/agent-home-delivery.mjs +57 -13
  148. package/src/session-runtime/agent-room-delivery.mjs +28 -1
  149. package/src/session-runtime/agent-task-sessions.mjs +292 -79
  150. package/src/session-runtime/agent-tool-routing.mjs +49 -4
  151. package/src/session-runtime/boot/begin.mjs +3 -2
  152. package/src/session-runtime/boot/tools.mjs +4 -0
  153. package/src/session-runtime/bot-chain.mjs +32 -0
  154. package/src/session-runtime/bot-status.mjs +7 -4
  155. package/src/session-runtime/channel-config-api.mjs +63 -17
  156. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
  157. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
  158. package/src/session-runtime/internal-tool-executor.mjs +4 -0
  159. package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -1
  160. package/src/session-runtime/mixy-heartbeat.mjs +807 -0
  161. package/src/session-runtime/model-route/route-tuning.mjs +10 -6
  162. package/src/session-runtime/owned-automation-run.mjs +33 -12
  163. package/src/session-runtime/provider-auth/login.mjs +7 -0
  164. package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
  165. package/src/session-runtime/routine-home-turn.mjs +33 -6
  166. package/src/session-runtime/schedule-session-run.mjs +7 -3
  167. package/src/session-runtime/services/agent-tool/close-flow.mjs +14 -2
  168. package/src/session-runtime/services/agent-tool/execute.mjs +6 -1
  169. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +11 -2
  170. package/src/session-runtime/services/agent-tool/render.mjs +3 -1
  171. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
  172. package/src/session-runtime/services/agent-tool/tool-def.mjs +1 -1
  173. package/src/session-runtime/services/channel-admin.mjs +66 -18
  174. package/src/session-runtime/services/channel-worker.mjs +1 -1
  175. package/src/session-runtime/services/provider-admin.mjs +128 -25
  176. package/src/session-runtime/services/schedule-access.mjs +14 -0
  177. package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
  178. package/src/session-runtime/session-create/session-options.mjs +7 -4
  179. package/src/session-runtime/settings-system-api.mjs +16 -1
  180. package/src/session-runtime/setup-tool/executor.mjs +49 -14
  181. package/src/session-runtime/setup-tool/extended-actions.mjs +29 -3
  182. package/src/session-runtime/setup-tool/settings-contract.mjs +65 -4
  183. package/src/session-runtime/setup-tool/tool-defs.mjs +30 -3
  184. package/src/session-runtime/task-session-route.mjs +99 -0
  185. package/src/session-runtime/tool-policy/policy-stage.mjs +5 -2
  186. package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
  187. package/src/session-runtime/tool-surface.mjs +4 -0
  188. package/src/session-runtime/usage-stats-api.mjs +1 -1
  189. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
  190. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
  191. package/src/session-runtime/workspace/transitions.mjs +10 -2
  192. package/src/standalone/daemon-session-control.mjs +88 -20
  193. package/src/standalone/daemon.mjs +8 -0
  194. package/src/standalone/session-protocol.mjs +5 -0
  195. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
  196. package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
  197. package/src/standalone/session-service.mjs +13 -0
  198. package/src/standalone/session-tool-output.mjs +8 -5
  199. package/src/tui/app/model-options.mjs +2 -0
  200. package/src/tui/dist/index.mjs +335 -172
  201. package/src/tui/session/notice-surface.mjs +17 -1
  202. package/src/tui/session/session-api/integrations.mjs +1 -0
  203. package/src/tui/session/session-api/lifecycle.mjs +8 -0
  204. package/src/tui/session/session-api/settings.mjs +14 -2
  205. package/src/tui/session/turn-stream.mjs +17 -1
  206. package/src/tui/session/turn.mjs +22 -2
  207. package/src/vendor/statusline/src/gateway/claude-current.mjs +0 -58
  208. package/src/vendor/statusline/src/gateway/route-meta.mjs +1 -361
  209. package/src/vendor/statusline/src/gateway/session-routes.mjs +0 -55
  210. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +0 -285
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { renameSync } from 'node:fs';
3
3
  import { modelProxyClientMode, serverModelCredentialsDisabledError } from '../../runtime/shared/model-proxy-mode.mjs';
4
4
  import { sameAccountIdentity } from '../../runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs';
5
+ import { agentApiKeyEnvName } from '../../runtime/shared/provider-api-key.mjs';
5
6
  import {
6
7
  AGENT_PROVIDER_ENV,
7
8
  SECRET_ACCOUNTS,
@@ -9,6 +10,7 @@ import {
9
10
  getAgentApiKey,
10
11
  getKeychainStatus,
11
12
  hasStoredSecret,
13
+ isFallbackSecret,
12
14
  saveSecretAsync,
13
15
  } from '../../runtime/shared/config.mjs';
14
16
  import {
@@ -80,6 +82,13 @@ import {
80
82
  hasCursorOAuthCredentials,
81
83
  loginCursorOAuth,
82
84
  } from '../../runtime/agent/orchestrator/providers/cursor-auth.mjs';
85
+ import {
86
+ beginDevinOAuthLogin,
87
+ describeDevinOAuthCredentials,
88
+ forgetDevinOAuthCredentials,
89
+ hasDevinOAuthCredentials,
90
+ loginDevinOAuth,
91
+ } from '../../runtime/agent/orchestrator/providers/devin-auth.mjs';
83
92
  import {
84
93
  describeAntigravityOAuthCredentials,
85
94
  forgetAntigravityOAuthCredentials,
@@ -100,6 +109,7 @@ import {
100
109
  removeProviderAccount,
101
110
  clearProviderAccountQuotaState,
102
111
  providerAccountLimit,
112
+ providerAccountBlockedUntil,
103
113
  ACCOUNT_PROVIDERS,
104
114
  } from '../../runtime/shared/provider-accounts.mjs';
105
115
  import { currentProviderAccountId, withProviderAccount } from '../../runtime/shared/provider-auth-binding.mjs';
@@ -214,6 +224,16 @@ const OAUTH_PROVIDERS = Object.freeze([
214
224
  begin: beginCursorOAuthLogin,
215
225
  login: loginCursorOAuth,
216
226
  }),
227
+ Object.freeze({
228
+ id: 'devin-oauth',
229
+ name: 'Devin',
230
+ desc: 'Sign in with your Devin account',
231
+ has: hasDevinOAuthCredentials,
232
+ describe: describeDevinOAuthCredentials,
233
+ forget: forgetDevinOAuthCredentials,
234
+ begin: beginDevinOAuthLogin,
235
+ login: loginDevinOAuth,
236
+ }),
217
237
  Object.freeze({
218
238
  id: 'antigravity-oauth',
219
239
  name: 'Antigravity OAuth',
@@ -231,7 +251,7 @@ const BUILTIN_PROVIDER_IDS = new Set(['mixdog-local']);
231
251
  const API_PROVIDER_IDS = new Set(API_PROVIDERS.map((p) => p.id));
232
252
  const OAUTH_BY_ID = new Map(OAUTH_PROVIDERS.map((p) => [p.id, p]));
233
253
 
234
- // Dev-only entries (cursor-oauth, antigravity-oauth, mimo-oauth) are unknown to
254
+ // Dev-only entries (cursor-oauth, devin-oauth, antigravity-oauth, mimo-oauth) are unknown to
235
255
  // settings/login while the Developer → Providers option is off. Filtered per
236
256
  // call, so toggling the option takes effect without a restart.
237
257
  function availableOAuthProviders() {
@@ -320,33 +340,71 @@ const KEYCHAIN_HINTS = Object.freeze({
320
340
  error: 'The system keyring could not be read — check that it is running and unlocked.',
321
341
  });
322
342
 
343
+ // The private-file store being ignored, by what is wrong with it (the repair
344
+ // differs: chmod cannot fix an owner or a symlink, and a save fails through a
345
+ // foreign or symlinked folder).
346
+ const UNSAFE_STORE_HINTS = Object.freeze({
347
+ permissions:
348
+ 'The private secrets folder has unsafe permissions, so its keys are ignored — run chmod 700 on the folder and chmod 600 on its files, or save the key again.',
349
+ owner:
350
+ 'The private secrets folder or one of its files belongs to another user, so its keys are ignored — change its owner to the current user (chown).',
351
+ type: 'The private secrets folder or one of its files is a symlink or not a regular file, so its keys are ignored — remove it and save the key again.',
352
+ });
353
+
323
354
  export function keychainIssueFor(checkSecrets) {
324
355
  if (!checkSecrets) return null;
325
- const { state } = getKeychainStatus();
356
+ // A host with no keychain provider keeps its keys in the private secrets
357
+ // file (keychain-cjs.cjs "Linux fallback file"): that works, so nothing to
358
+ // report — unless the store's permissions make reads ignore it, which is
359
+ // reported with the folder to repair. A locked keyring is still an issue.
360
+ const { state, fallback, unsafeStore } = getKeychainStatus();
361
+ if (fallback && unsafeStore) {
362
+ const { kind, path } = unsafeStore;
363
+ return { state: 'unsafe-store', kind, message: UNSAFE_STORE_HINTS[kind] ?? UNSAFE_STORE_HINTS.permissions, path };
364
+ }
365
+ if (fallback) return null;
326
366
  const message = KEYCHAIN_HINTS[state];
327
367
  return message ? { state, message } : null;
328
368
  }
329
369
 
330
- function apiProviderSetup(p, providers, checkSecrets) {
370
+ // Where a stored key lives, for a row's detail line: decided per account, so a
371
+ // key that is still only in the fallback file reads as such after the keychain
372
+ // recovers.
373
+ function storedSecretDetail(account) {
374
+ return isFallbackSecret(account) ? 'stored in private file' : 'stored in keychain';
375
+ }
376
+
377
+ // `keychainIssue`: the setup response's one keyring check (never asked per row).
378
+ function apiProviderSetup(p, providers, checkSecrets, keychainIssue) {
331
379
  const configured = providers[p.id] || {};
332
- const envName = AGENT_PROVIDER_ENV[p.id] || p.env;
333
- const env = Boolean(envName && process.env[envName]);
380
+ // The environment variable the key resolver actually reads (aliases included);
381
+ // it wins over a stored key, so provenance follows it, not the stored secret.
382
+ const resolvedEnv = agentApiKeyEnvName(p.id);
383
+ const declaredEnv = AGENT_PROVIDER_ENV[p.id] || p.env;
384
+ const envName = resolvedEnv || declaredEnv;
385
+ const env = Boolean(resolvedEnv || (declaredEnv && process.env[declaredEnv]));
334
386
  const configuredEnabled = configured.enabled === true;
335
387
  const stored = checkSecrets ? hasStoredSecret(SECRET_ACCOUNTS.agentApiKey(p.id)) : false;
336
388
  const authenticated = env || stored || (checkSecrets ? Boolean(getAgentApiKey(p.id)) : configuredEnabled);
337
389
  let status = configuredEnabled ? 'No Key' : 'Off';
338
- if (stored) status = 'Set';
339
- else if (env) status = 'Env';
340
- else if (authenticated) status = 'Set';
390
+ if (env) status = 'Env';
391
+ else if (stored || authenticated) status = 'Set';
392
+ // Where the effective credential comes from: environment | private-file | keychain.
393
+ let credentialSource = null;
341
394
  let detail = envName;
342
- if (stored) detail = 'stored in keychain';
343
- else if (env) detail = envName;
344
- else if (authenticated) detail = 'runtime credential';
345
- const keychainIssue = authenticated ? null : keychainIssueFor(checkSecrets);
346
- if (keychainIssue) detail = keychainIssue.message;
395
+ if (env) {
396
+ credentialSource = 'environment';
397
+ } else if (stored) {
398
+ detail = storedSecretDetail(SECRET_ACCOUNTS.agentApiKey(p.id));
399
+ credentialSource = detail === 'stored in private file' ? 'private-file' : 'keychain';
400
+ } else if (authenticated) {
401
+ detail = 'runtime credential';
402
+ }
403
+ const rowIssue = authenticated ? null : keychainIssue;
404
+ if (rowIssue) detail = rowIssue.message;
347
405
  return {
348
406
  ...p,
349
- ...(keychainIssue ? { keychainIssue } : {}),
407
+ ...(rowIssue ? { keychainIssue: rowIssue } : {}),
350
408
  group: 'api',
351
409
  type: 'api-key',
352
410
  enabled: configuredEnabled || authenticated,
@@ -356,6 +414,7 @@ function apiProviderSetup(p, providers, checkSecrets) {
356
414
  envName,
357
415
  status,
358
416
  detail,
417
+ ...(credentialSource ? { credentialSource } : {}),
359
418
  };
360
419
  }
361
420
 
@@ -469,7 +528,7 @@ function customProviderRow(id, entry, checkSecrets = true) {
469
528
  const stored = checkSecrets ? hasStoredSecret(SECRET_ACCOUNTS.agentApiKey(id)) : false;
470
529
  const unstoredStatus = configuredEnabled ? 'No Key' : 'Off';
471
530
  let status = stored ? 'Set' : unstoredStatus;
472
- let detail = stored ? 'stored in keychain' : 'custom endpoint';
531
+ let detail = stored ? storedSecretDetail(SECRET_ACCOUNTS.agentApiKey(id)) : 'custom endpoint';
473
532
  if (apiKeyEnv) {
474
533
  status = envSet ? 'Env' : unstoredStatus;
475
534
  detail = apiKeyEnv;
@@ -533,7 +592,7 @@ export async function providerSetup(config = {}, options = {}) {
533
592
  return {
534
593
  ...(keychain ? { keychain } : {}),
535
594
  api: [
536
- ...API_PROVIDERS.map((p) => apiProviderSetup(p, providers, checkSecrets)),
595
+ ...API_PROVIDERS.map((p) => apiProviderSetup(p, providers, checkSecrets, keychain)),
537
596
  ...customProviderRows(providers, checkSecrets),
538
597
  ],
539
598
  oauth: availableOAuthProviders().map((p) => oauthProviderSetup(p, providers, checkSecrets)),
@@ -885,7 +944,8 @@ export function listProviderAccounts(provider) {
885
944
  authenticated: auth.authenticated === true,
886
945
  reauthRequired: auth.reauthRequired === true,
887
946
  usage: row.usage || null,
888
- blockedUntil: row.blockedUntil || null,
947
+ // When the limit shown lifts: the latest deadline of every block.
948
+ blockedUntil: providerAccountBlockedUntil(row) || null,
889
949
  // The limit holding the account now: account-wide, or only for one
890
950
  // model family ("7D Fable") while other models still run.
891
951
  limit: providerAccountLimit(row, now, { useCredits: pool.useCredits === true }),
@@ -1113,6 +1173,44 @@ export async function saveCustomProvider(cfgMod, rawInput) {
1113
1173
  return typed ? serializeCustomKeySave(id, commit) : commit();
1114
1174
  }
1115
1175
 
1176
+ // Replaces only the stored key of an existing custom provider. The provider's
1177
+ // settings are never read back and rewritten, so a concurrent edit of them
1178
+ // (or a removal) is never reverted.
1179
+ export async function saveCustomProviderKey(cfgMod, rawId, rawKey) {
1180
+ requireLocalProviderManagement();
1181
+ const id = String(rawId || '').trim();
1182
+ if (typeof rawKey !== 'string' || rawKey.length > CUSTOM_API_KEY_MAX) throw new TypeError('custom provider apiKey is invalid');
1183
+ const key = rawKey.trim();
1184
+ if (!key) throw new Error('API key is required');
1185
+ const takesStoredKey = () => {
1186
+ const entry = existingCustomEntry(cfgMod.loadConfig(), id);
1187
+ const config = storedCustomConfig(entry);
1188
+ if ((config?.auth ?? entry.auth) === 'none') throw new Error('this custom provider needs no API key');
1189
+ if (config?.apiKeyEnv) throw new Error(`this custom provider reads its key from ${config.apiKeyEnv}`);
1190
+ return entry;
1191
+ };
1192
+ takesStoredKey();
1193
+ // The provider's removal runs in the same queue, so it never lands between
1194
+ // the checks and the write below; a removal queued earlier is seen by the
1195
+ // check, before anything is written.
1196
+ return serializeCustomKeySave(id, async () => {
1197
+ takesStoredKey();
1198
+ const account = SECRET_ACCOUNTS.agentApiKey(id);
1199
+ const previousKey = getAgentApiKey(id);
1200
+ await saveSecretAsync(account, key);
1201
+ try {
1202
+ return customProviderRow(id, takesStoredKey());
1203
+ } catch (error) {
1204
+ // Switched to keyless (or the provider is gone) meanwhile: the key written
1205
+ // is dropped, and the previous one is only put back for a provider that exists.
1206
+ const exists = isCustomProviderConfig(cfgMod.loadConfig().providers?.[id]);
1207
+ if (exists && previousKey) await saveSecretAsync(account, previousKey);
1208
+ else await deleteSecretAsync(account);
1209
+ throw error;
1210
+ }
1211
+ });
1212
+ }
1213
+
1116
1214
  const MODEL_PATCH_KEYS = new Set([
1117
1215
  'manual',
1118
1216
  'hidden',
@@ -1292,14 +1390,19 @@ export async function removeCustomProvider(cfgMod, rawId) {
1292
1390
  requireLocalProviderManagement();
1293
1391
  const id = String(rawId || '').trim();
1294
1392
  existingCustomEntry(cfgMod.loadConfig(), id);
1295
- // Secret first: a real keychain failure (locked, permission) leaves the
1296
- // provider registered so the removal can be retried. A missing secret
1297
- // (keyless provider) is not an error.
1298
- await deleteSecretAsync(SECRET_ACCOUNTS.agentApiKey(id));
1299
- const current = cfgMod.loadConfig();
1300
- const { [id]: _removed, ...providers } = current.providers || {};
1301
- cfgMod.saveConfig(withoutProviderReferences({ ...current, providers }, id), { baseConfig: current });
1302
- return { provider: id, type: 'custom', removed: true };
1393
+ // In the provider's key queue: a key write in flight finishes first and
1394
+ // none starts between the secret's deletion and the config's.
1395
+ return serializeCustomKeySave(id, async () => {
1396
+ existingCustomEntry(cfgMod.loadConfig(), id);
1397
+ // Secret first: a real keychain failure (locked, permission) leaves the
1398
+ // provider registered so the removal can be retried. A missing secret
1399
+ // (keyless provider) is not an error.
1400
+ await deleteSecretAsync(SECRET_ACCOUNTS.agentApiKey(id));
1401
+ const current = cfgMod.loadConfig();
1402
+ const { [id]: _removed, ...providers } = current.providers || {};
1403
+ cfgMod.saveConfig(withoutProviderReferences({ ...current, providers }, id), { baseConfig: current });
1404
+ return { provider: id, type: 'custom', removed: true };
1405
+ });
1303
1406
  }
1304
1407
 
1305
1408
  export async function testCustomProvider(cfgMod, rawInput) {
@@ -21,6 +21,7 @@ import { MIXY_AGENT_ID, agentIdOfHomeSession } from '../../runtime/agent/orchest
21
21
  import { roomMemberOfSession } from '../../runtime/agent/orchestrator/session/agent-room.mjs';
22
22
  import { mixyDisplayName } from '../../runtime/agent/orchestrator/session/mixy-name.mjs';
23
23
  import { isAgentOwner } from '../../runtime/agent/orchestrator/agent-owner.mjs';
24
+ import { isHeartbeatSession } from '../../runtime/shared/routine-trigger.mjs';
24
25
  import { unattendedRunOf } from '../held-requests.mjs';
25
26
 
26
27
  const INTERACTIVE_SOURCE_TYPES = new Set(['', 'lead', 'cli']);
@@ -40,6 +41,8 @@ function provenanceScope(session, id) {
40
41
  if (member) return { kind: 'room-member', agentId: member.agentId };
41
42
  const homeAgent = agentIdOfHomeSession(id);
42
43
  if (homeAgent) return homeAgent === MIXY_AGENT_ID ? { kind: 'human' } : { kind: 'agent', agentId: homeAgent };
44
+ // Mixy's hidden heartbeat session: her own scope, as her chat's routine turns get.
45
+ if (isHeartbeatSession(id)) return { kind: 'agent', agentId: MIXY_AGENT_ID };
43
46
  const headless =
44
47
  !id ||
45
48
  isAgentOwner(session) ||
@@ -108,6 +111,17 @@ export function assertOwnAgentSettings(scope, agentId, action) {
108
111
  }
109
112
  }
110
113
 
114
+ /** Throws unless Mixy's heartbeat session `sessionId` may take setup `action`
115
+ * with `args`: on her own she only pauses a routine or runs one again; every
116
+ * other change she proposes (MIXY.md). Any other session passes. */
117
+ export function assertHeartbeatSetupAction(sessionId, action, args) {
118
+ if (!isHeartbeatSession(sessionId)) return;
119
+ if (action === 'run_automation' || (action === 'set_automation_enabled' && args?.enabled === false)) return;
120
+ throw new Error(
121
+ `A heartbeat turn only pauses a routine or runs one again (setup ${action} is refused); propose other changes with session action suggest`
122
+ );
123
+ }
124
+
111
125
  /** Throws when `scope` is a headless run: it reads settings, never changes
112
126
  * them; an unattended run changes only its own bot's automations and route. */
113
127
  export function assertInteractiveCaller(scope, action) {
@@ -348,6 +348,7 @@ const PROVIDER_RANKS = {
348
348
  'command-code-oauth': 33.5,
349
349
  'mimo-oauth': 34,
350
350
  'cursor-oauth': 35,
351
+ 'devin-oauth': 35.5,
351
352
  'antigravity-oauth': 36,
352
353
  'opencode-go-oauth': 38,
353
354
  openai: 50,
@@ -9,7 +9,7 @@ import { readAgentFrontmatterPermission } from '../services/agent-tool/helpers.m
9
9
  import { LEAD_DISALLOWED_TOOLS } from '../../runtime/agent/orchestrator/runtime-core/tool-defs.mjs';
10
10
  import { HEADLESS_TOOL_PROFILE } from '../../runtime/agent/orchestrator/runtime-core/tool-profile.mjs';
11
11
  import { AGENT_MODE, resolveSessionMode } from '../../runtime/agent/orchestrator/session/agent-mode.mjs';
12
- import { MIXY_AGENT_ID, agentIdOfHomeSession } from '../../runtime/agent/orchestrator/session/agent-home.mjs';
12
+ import { agentIdOfHomeSession } from '../../runtime/agent/orchestrator/session/agent-home.mjs';
13
13
  import { roomMemberOfSession } from '../../runtime/agent/orchestrator/session/agent-room.mjs';
14
14
  import { unattendedApprovalPort } from '../../runtime/agent/orchestrator/session/human-gate/gate.mjs';
15
15
 
@@ -70,7 +70,7 @@ export function sessionOptions(deps, coreMemoryContext) {
70
70
  const workflowContext = packContext;
71
71
  const leadWorkflowParts = agentOwned
72
72
  ? undefined
73
- : leadPromptParts({ agentMode: sessionMode === AGENT_MODE, mixy: actingAgent === MIXY_AGENT_ID });
73
+ : leadPromptParts({ agentMode: sessionMode === AGENT_MODE, agentId: actingAgent });
74
74
  const callerDisallowedTools = [
75
75
  ...(agentOwned ? [] : LEAD_DISALLOWED_TOOLS),
76
76
  ...(!agentOwned && rt.disallowDelegation ? ['agent'] : []),
@@ -88,17 +88,20 @@ export function sessionOptions(deps, coreMemoryContext) {
88
88
  lane: agentOwned ? 'agent' : 'cli',
89
89
  // One-shot `mixdog exec` runs carry their own source so session catalogs
90
90
  // can keep them out of the user's resumable list.
91
+ // A hidden run of an agent (Mixy's heartbeat) is an exec run too.
91
92
  sourceType: agentOwned
92
93
  ? sessionProfile?.sourceType || 'agent'
93
- : rt.toolProfile === HEADLESS_TOOL_PROFILE
94
+ : rt.toolProfile === HEADLESS_TOOL_PROFILE || sessionProfile?.sourceType === 'exec'
94
95
  ? 'exec'
95
96
  : 'lead',
96
97
  sourceName: agentOwned
97
98
  ? sessionProfile?.sourceName || sessionProfile?.agent || 'agent'
98
99
  : taskOrigin
99
100
  ? `agent:${taskOrigin.agentId}${taskOriginSuffix(taskOrigin)}`
100
- : 'main',
101
+ : clean(sessionProfile?.sourceName) || 'main',
101
102
  ...(taskOrigin ? { taskOrigin } : {}),
103
+ // The agent a run acts for (agent mode, its memory; agent-mode.mjs).
104
+ ...(!agentOwned && clean(sessionProfile?.ownerAgentId) ? { ownerAgentId: clean(sessionProfile.ownerAgentId) } : {}),
102
105
  ...(rt.approvalMode ? { approvalMode: rt.approvalMode } : {}),
103
106
  clientHostPid: sessionProfile?.clientHostPid || process.pid,
104
107
  mcpScopeId: rt.mcpScopeId,
@@ -1,4 +1,5 @@
1
- // Automation worker status, the system shell and self-update settings.
1
+ // Automation worker status, the system shell, self-update, Mixy's heartbeat
2
+ // and bots' reactions settings.
2
3
  export function createSystemSettings({
3
4
  getConfig,
4
5
  saveConfigAndAdopt,
@@ -49,6 +50,20 @@ export function createSystemSettings({
49
50
  });
50
51
  return this.getUpdateSettings();
51
52
  },
53
+ getHeartbeatSettings() {
54
+ return { heartbeat: getConfig()?.heartbeat !== false };
55
+ },
56
+ setHeartbeatEnabled(enabled) {
57
+ saveConfigAndAdopt({ ...getConfig(), heartbeat: enabled === true });
58
+ return this.getHeartbeatSettings();
59
+ },
60
+ getReactionSettings() {
61
+ return { reactions: getConfig()?.reactions !== false };
62
+ },
63
+ setReactionsEnabled(enabled) {
64
+ saveConfigAndAdopt({ ...getConfig(), reactions: enabled === true });
65
+ return this.getReactionSettings();
66
+ },
52
67
  async checkForUpdate(options = {}) {
53
68
  await checkForUpdateInternal({ force: options?.force === true });
54
69
  return this.getUpdateSettings();
@@ -12,6 +12,7 @@ import { SETUP_DESKTOP_DOMAINS, SETUP_HANDOFFS } from './settings-contract.mjs';
12
12
  import { executeExtendedSetupAction, publicAutomation, publicHookRule, validateMcpInput } from './extended-actions.mjs';
13
13
  import {
14
14
  assertAgentEditorAccess,
15
+ assertHeartbeatSetupAction,
15
16
  assertInteractiveCaller,
16
17
  assertOwnAgentSettings,
17
18
  scheduleCallerScope,
@@ -44,6 +45,12 @@ const ACTION_APPLIES_TO = {
44
45
  "new sessions and an empty current one (the default mode); a conversation keeps its own mode until it is switched with the composer's mode selector",
45
46
  set_recap_enabled: 'background Memory cycles; no restart required',
46
47
  set_auto_update: 'subsequent automatic update checks',
48
+ set_ai_session_titles: 'titles generated from now on; existing titles are unchanged',
49
+ set_quota_auto_resume: 'subsequent usage-limit stops; turning it off cancels pending resumes',
50
+ set_model_preferences: 'the /model picker and model management lists',
51
+ set_heartbeat_enabled: "Mixy's next heartbeat check (every 30 minutes); no restart required",
52
+ set_reactions_enabled:
53
+ "bots' chats; while off a reaction is refused at once and new chats are not offered the tool; reactions already left stay visible",
47
54
  set_worktree_settings: 'new tasks and worktrees created afterwards; existing worktrees are unchanged',
48
55
  set_system_shell: 'subsequent shell calls',
49
56
  save_automation: 'the automation worker; scheduled runs follow the saved timing',
@@ -54,7 +61,7 @@ const ACTION_APPLIES_TO = {
54
61
  add_hook_rule: 'subsequent tool calls; no restart required',
55
62
  set_hook_rule_enabled: 'subsequent tool calls; no restart required',
56
63
  delete_hook_rule: 'subsequent tool calls; no restart required',
57
- set_media_default: 'media generations that omit lane/model, and the Studio selection',
64
+ set_media_default: 'media generations that name no lane/model; lane "" returns the kind to Auto',
58
65
  };
59
66
 
60
67
  const OPEN_TARGET_HINTS = Object.freeze({
@@ -65,11 +72,11 @@ const OPEN_TARGET_HINTS = Object.freeze({
65
72
  workflow: 'Desktop: Workflows · TUI: /workflow',
66
73
  agents: 'Desktop: Workflows → Agents · TUI: /agents',
67
74
  outputstyle: 'Desktop: Settings → Output style · TUI: /OutputStyle',
68
- theme: 'Desktop: Settings → General → Theme · TUI: /theme',
75
+ theme: 'Desktop: Settings → Theme · TUI: /theme',
69
76
  profile: 'Desktop: Settings → General → Profile · TUI: /profile',
70
77
  autoclear: 'Desktop: Settings → Context · TUI: /autoclear',
71
78
  memory:
72
- 'Desktop: Extensions → Plugin → Built-in (Memory); core memories under Projects → project → Memories · TUI: /memory',
79
+ 'Desktop: Extensions tab → Built-in (Memory); core memories under Projects → project → Memories · TUI: /memory',
73
80
  mcp: 'Desktop: Extensions → Connectors tab · TUI: /mcp',
74
81
  skills: 'Desktop: Skills (activity rail) · TUI: /skills',
75
82
  plugins:
@@ -79,7 +86,7 @@ const OPEN_TARGET_HINTS = Object.freeze({
79
86
  doctor: 'Desktop: Settings → System → Doctor, or /doctor in the composer · TUI: /doctor',
80
87
  context: 'Desktop: /context in the composer · TUI: /context',
81
88
  developer: 'Desktop: Settings → Developer options · TUI: /setting → Developer',
82
- voice: 'Desktop: Extensions → Plugin → Built-in (Voice) · TUI: /setting → Voice',
89
+ voice: 'Desktop: Extensions tab → Built-in (Voice) · TUI: /setting → Voice',
83
90
  connection: 'Desktop only: Settings → Connection',
84
91
  });
85
92
 
@@ -277,16 +284,20 @@ async function featureStatus(rt, getConfig) {
277
284
  browser: { active: builtinFeatureActive(config, 'browser') },
278
285
  computer: { active: builtinFeatureActive(config, 'computer') },
279
286
  terminal: { active: builtinFeatureActive(config, 'terminal') },
287
+ heartbeat: { enabled: config?.heartbeat !== false },
288
+ reactions: { enabled: config?.reactions !== false },
280
289
  voice: rt.getVoiceStatus ? voiceFeatureStatus(await rt.getVoiceStatus()) : null,
281
290
  };
282
291
  }
283
292
 
284
293
  async function mediaDefaults(rt) {
285
- const rows = await Promise.all(MEDIA_KINDS.map((kind) => rt.getMediaDefault(kind)));
294
+ const [pins, ...rows] = await Promise.all([rt.getMediaPins(), ...MEDIA_KINDS.map((kind) => rt.getMediaDefault(kind))]);
295
+ const fieldsOf = (row, fields) => (row ? Object.fromEntries(fields.map((field) => [field, row[field]])) : null);
286
296
  return {
287
- defaults: Object.fromEntries(
288
- MEDIA_KINDS.map((kind, i) => [kind, rows[i] ? { lane: rows[i].lane, model: rows[i].model } : null])
289
- ),
297
+ // The default generator (Settings → Built-in → Media generation); null = Auto.
298
+ pinned: Object.fromEntries(MEDIA_KINDS.map((kind) => [kind, fieldsOf(pins[kind], ['lane', 'model', 'account'])])),
299
+ // The last generator used; applies only to a kind that is on Auto.
300
+ remembered: Object.fromEntries(MEDIA_KINDS.map((kind, i) => [kind, fieldsOf(rows[i], ['lane', 'model'])])),
290
301
  };
291
302
  }
292
303
 
@@ -324,7 +335,15 @@ const SETUP_STATUS_READERS = {
324
335
  features: await featureStatus(rt, getConfig),
325
336
  onboarding: rt.getOnboardingStatus?.() || null,
326
337
  }),
327
- model: (rt) => ({ route: mainRoute(rt), effortOptions: rt.effortOptions || [] }),
338
+ model: (rt) => ({
339
+ route: {
340
+ ...mainRoute(rt),
341
+ autoEffort: rt.autoEffort !== false,
342
+ // The Lead preset (workflow-lead) holds the Lead route's fallback chain, as the Workflows page reads it.
343
+ fallbacks: (rt.listPresets?.() || []).find((preset) => preset?.id === 'workflow-lead')?.fallbacks || [],
344
+ },
345
+ effortOptions: rt.effortOptions || [],
346
+ }),
328
347
  agents: (rt) => ({
329
348
  agents: rt.listAgents?.() || [],
330
349
  orchestrationMode: rt.getOrchestrationMode(),
@@ -347,19 +366,23 @@ const SETUP_STATUS_READERS = {
347
366
  };
348
367
  },
349
368
  autoclear: (rt) => rt.getAutoClear?.() || {},
350
- compaction: (rt) => rt.getCompactionSettings?.() || {},
369
+ compaction: (rt) => ({ ...(rt.getCompactionSettings?.() || {}), ...(rt.getSessionTitleSettings?.() || {}) }),
351
370
  memory: (rt) => ({ ...(rt.getToolModuleSettings?.()?.memory || {}), recap: rt.getRecapSettings?.() || null }),
352
371
  'local-provider': (rt) => rt.getToolModuleSettings().localProvider,
353
372
  features: (rt, { getConfig }) => featureStatus(rt, getConfig),
354
373
  shell: (rt) => rt.getSystemShell?.() || {},
355
374
  providers: async (rt, { detail }) => {
356
375
  const rows = publicProviderRows(await rt.getProviderSetup?.({}));
376
+ const quotaResume = rt.getQuotaAutoResume ? { quotaAutoResume: rt.getQuotaAutoResume() } : {};
357
377
  // Client mode: the desktop owns accounts, so the server roster is never read.
358
378
  if (rows.managedByClient) {
359
- if (detail) return rows;
360
- return { managedByClient: true, unavailable: rows.unavailable, ...configuredProviderRows(rows) };
379
+ if (detail) return { ...rows, ...quotaResume };
380
+ return { managedByClient: true, unavailable: rows.unavailable, ...configuredProviderRows(rows), ...quotaResume };
361
381
  }
362
- if (!detail) return configuredProviderRows(rows, (id) => rt.getProviderAccounts(id).accounts.length);
382
+ if (!detail) {
383
+ return { ...configuredProviderRows(rows, (id) => rt.getProviderAccounts(id).accounts.length), ...quotaResume };
384
+ }
385
+ Object.assign(rows, quotaResume);
363
386
  rows.oauth = rows.oauth.map((row) =>
364
387
  row.authenticated ? { ...row, accounts: publicAccounts(rt.getProviderAccounts(row.id)) } : row
365
388
  );
@@ -597,6 +620,16 @@ const SETUP_ACTION_HANDLERS = {
597
620
  rt.deleteLocalProviderModel(requireText(args.confirmationToken, 'confirmationToken')),
598
621
  set_system_shell: (rt, args) => rt.setSystemShell({ command: clean(args.command) }),
599
622
  set_auto_update: (rt, args) => rt.setAutoUpdate(requireBoolean(args.enabled)),
623
+ set_ai_session_titles: (rt, args) => rt.setAiSessionTitles(requireBoolean(args.enabled)),
624
+ set_quota_auto_resume: async (rt, args) => ({ quotaAutoResume: await rt.setQuotaAutoResume(requireBoolean(args.enabled)) }),
625
+ set_model_preferences: async (rt, args) => {
626
+ const change = { model: requireText(args.model, 'model') };
627
+ for (const key of ['hidden', 'favorite']) if (Object.hasOwn(args, key)) change[key] = requireBoolean(args[key], key);
628
+ if (Object.keys(change).length === 1) throw new Error('hidden or favorite is required');
629
+ return rt.setModelPreferences(requireText(args.provider, 'provider'), [change]);
630
+ },
631
+ set_heartbeat_enabled: (rt, args) => rt.setHeartbeatEnabled(requireBoolean(args.enabled)),
632
+ set_reactions_enabled: (rt, args) => rt.setReactionsEnabled(requireBoolean(args.enabled)),
600
633
  set_worktree_settings: (rt, args) => rt.setWorktreeSettings(args.worktree),
601
634
  forget_provider_auth: async (rt, args) => {
602
635
  const name = requireText(args.name, 'name');
@@ -666,7 +699,7 @@ const SETUP_ACTION_HANDLERS = {
666
699
  // as Extensions → 커넥터. Nothing is connected here.
667
700
  connect_connector: async (rt, args) => {
668
701
  const entry = connectorById(requireText(args.connector, 'connector'));
669
- if (!entry || entry.ownKey) throw new Error(`connector must be one of ${CONNECTOR_IDS.join(', ')}`);
702
+ if (!entry) throw new Error(`connector must be one of ${CONNECTOR_IDS.join(', ')}`);
670
703
  const server = (rt.mcpStatus?.()?.servers || []).find((row) => row.connector === entry.id) || null;
671
704
  const connected = server?.connected === true;
672
705
  return {
@@ -786,6 +819,8 @@ export function createSetupToolExecutor({
786
819
  }
787
820
  if (action === 'set_agent_route') assertOwnAgentSettings(scope, args.agent, action);
788
821
  }
822
+ // Mixy's heartbeat session takes only its two routine actions, reads included.
823
+ assertHeartbeatSetupAction(sessionId, action, args);
789
824
  const rt = action === 'status' || action === 'open' ? null : api();
790
825
  if (!Object.hasOwn(SETUP_ACTION_HANDLERS, action)) {
791
826
  return executeExtendedSetupAction(rt, args, requestDesktop, { sessionId, caller });
@@ -33,9 +33,16 @@ function compactModelRow(row) {
33
33
  efforts: (row.effortOptions || []).map((option) => option.value),
34
34
  ...(row.fastCapable === true ? { fast: true } : {}),
35
35
  ...(row.supportsVision === true ? { vision: true } : {}),
36
+ ...modelPreferenceFields(row),
36
37
  };
37
38
  }
38
39
 
40
+ /** The model-management choices, only when set (savedHidden is an explicit /model visibility choice). */
41
+ const modelPreferenceFields = (row) => ({
42
+ ...(typeof row.savedHidden === 'boolean' ? { hidden: row.savedHidden } : {}),
43
+ ...(row.favorite === true ? { favorite: true } : {}),
44
+ });
45
+
39
46
  const isPresent = (value) =>
40
47
  value != null && value !== '' && value !== false && !(typeof value === 'object' && !Object.keys(value).length);
41
48
 
@@ -64,7 +71,7 @@ function modelDetail(row) {
64
71
  defaultModelParameters: row.defaultModelParameters,
65
72
  supportsMaxMode: row.supportsMaxMode,
66
73
  };
67
- return Object.fromEntries(Object.entries(detail).filter(([, value]) => isPresent(value)));
74
+ return { ...Object.fromEntries(Object.entries(detail).filter(([, value]) => isPresent(value))), ...modelPreferenceFields(row) };
68
75
  }
69
76
 
70
77
  function findModel(rows, id, provider) {
@@ -283,6 +290,7 @@ export const EXTENDED_SETUP_HANDLERS = Object.freeze({
283
290
  if (kind === 'webhook' && ['time', 'at', 'timezone', 'days'].some((key) => own(input, key))) {
284
291
  throw new Error('Schedule fields (timing) are not accepted for webhooks');
285
292
  }
293
+ if (kind === 'webhook' && own(input, 'newName')) throw new Error('newName (rename) is schedule-only');
286
294
  if (kind === 'schedule' && own(input, 'parser')) throw new Error('parser is a webhook-only field');
287
295
  let previous = {};
288
296
  if (input.overwrite) {
@@ -363,6 +371,12 @@ export const EXTENDED_SETUP_HANDLERS = Object.freeze({
363
371
  .sections.flatMap((section) => section.options)
364
372
  .find((row) => row.id === args.name);
365
373
  if (!option) throw new Error(`Unknown developer option "${args.name}"; read status developer`);
374
+ // An agent must never remove its own approval cards: only the person turns this on.
375
+ if (args.enabled && option.id === 'allowAllActions') {
376
+ throw new Error(
377
+ `"${option.label}" cannot be turned on by an agent; only the person can turn it on in Settings → Developer options.`
378
+ );
379
+ }
366
380
  if (args.enabled && option.warning && !args.riskAccepted) {
367
381
  throw new Error(
368
382
  `Enabling ${option.label} requires riskAccepted:true after the user explicitly accepts: ${option.warning}`
@@ -401,7 +415,12 @@ export const EXTENDED_SETUP_HANDLERS = Object.freeze({
401
415
  },
402
416
  async set_media_default(rt, args) {
403
417
  if (!MEDIA_KINDS.includes(args.kind)) throw new Error(`kind must be one of ${MEDIA_KINDS.join(', ')}`);
404
- const lane = (await rt.listMediaLanes()).find((row) => row.id === args.lane);
418
+ // A blank lane returns the kind to Auto, as the Settings control does.
419
+ if (!String(args.lane).trim()) {
420
+ if (own(args, 'model') || own(args, 'accountId')) throw new Error('model and accountId need a lane');
421
+ return { kind: args.kind, media: await rt.setMediaPin({ kind: args.kind, lane: '' }) };
422
+ }
423
+ const lane =(await rt.listMediaLanes()).find((row) => row.id === args.lane);
405
424
  if (!lane) throw new Error(`Unknown media lane "${args.lane}"; list lanes with the media tool`);
406
425
  if (!lane.authenticated) throw new Error(`${lane.label} is not signed in; open providers first`);
407
426
  const spec = lane[args.kind];
@@ -409,7 +428,14 @@ export const EXTENDED_SETUP_HANDLERS = Object.freeze({
409
428
  if (own(args, 'model') && !spec.models.some((model) => model.id === args.model)) {
410
429
  throw new Error(`model "${args.model}" is not available on ${lane.id}/${args.kind}`);
411
430
  }
412
- return { media: await rt.setMediaDefault(pick(args, ['kind', 'lane', 'model'])) };
431
+ if (own(args, 'accountId')) {
432
+ if (lane.authType !== 'oauth') throw new Error(`${lane.id} signs in with an API key; it has no accounts`);
433
+ if (!rt.getProviderAccounts(lane.authProvider).accounts.some((row) => row.id === args.accountId)) {
434
+ throw new Error(`Unknown ${lane.authProvider} account "${args.accountId}"; list accounts with status providers`);
435
+ }
436
+ }
437
+ const pin = await rt.setMediaPin({ kind: args.kind, lane: args.lane, model: args.model, account: args.accountId });
438
+ return { kind: args.kind, media: pin };
413
439
  },
414
440
  });
415
441