mixdog 1.2.1 → 1.2.2

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 (233) hide show
  1. package/package.json +1 -1
  2. package/scripts/gate-local.mjs +19 -3
  3. package/scripts/model-catalog-audit.mjs +1 -15
  4. package/src/agents/reviewer/AGENT.md +3 -3
  5. package/src/agents/reviewer/agent.json +1 -1
  6. package/src/defaults/shipped-history.json +1 -0
  7. package/src/repl/slash-commands.mjs +16 -1
  8. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -4
  9. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +10 -0
  10. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +36 -7
  11. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +49 -10
  12. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +40 -3
  13. package/src/runtime/agent/orchestrator/mcp/client.mjs +230 -96
  14. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +103 -0
  15. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +150 -0
  16. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +111 -0
  17. package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +87 -0
  18. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +95 -0
  19. package/src/runtime/agent/orchestrator/mcp/oauth-request.mjs +122 -0
  20. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +9 -0
  21. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +229 -0
  22. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +188 -29
  23. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +2 -1
  24. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +94 -31
  25. package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +96 -0
  26. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +39 -18
  27. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +55 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +25 -21
  29. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +3 -1
  30. package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +18 -4
  31. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +13 -1
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-midstream.mjs +10 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-recovery.mjs +23 -9
  35. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +46 -20
  36. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +14 -9
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +73 -35
  38. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +69 -0
  39. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +4 -0
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -1
  41. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -7
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +30 -9
  43. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  44. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +30 -14
  45. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +2 -1
  46. package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +93 -0
  47. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +82 -9
  48. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +3 -0
  49. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +38 -5
  50. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  51. package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +97 -2
  52. package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +102 -23
  53. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +103 -33
  54. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +29 -6
  55. package/src/runtime/agent/orchestrator/providers/cursor.mjs +18 -1
  56. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +77 -17
  57. package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +29 -8
  59. package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +191 -0
  60. package/src/runtime/agent/orchestrator/providers/gemini-family.mjs +9 -0
  61. package/src/runtime/agent/orchestrator/providers/gemini-history-signatures.mjs +9 -4
  62. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +180 -0
  64. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +21 -82
  65. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +4 -1
  66. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -12
  67. package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +21 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +56 -23
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  70. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +68 -19
  71. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +3 -0
  72. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +2 -1
  73. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +4 -9
  74. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +4 -2
  75. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +22 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +1 -0
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +50 -0
  78. package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +23 -0
  79. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +16 -8
  80. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-state.mjs +3 -2
  81. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +94 -13
  82. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +193 -0
  83. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +66 -2
  84. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -1
  85. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +6 -0
  86. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +4 -2
  87. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -14
  88. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +17 -3
  89. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +42 -25
  90. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +274 -0
  91. package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +72 -32
  92. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +18 -11
  93. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +1 -2
  94. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-stamps.mjs +2 -2
  95. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +2 -3
  96. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +2 -2
  97. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +1 -3
  98. package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +34 -3
  99. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +5 -1
  100. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +22 -2
  101. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -0
  102. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +5 -9
  103. package/src/runtime/agent/orchestrator/providers/openai-retry-policy.mjs +9 -0
  104. package/src/runtime/agent/orchestrator/providers/openai-ws-handshake.mjs +16 -8
  105. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +1 -2
  106. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/policy.mjs +25 -14
  107. package/src/runtime/agent/orchestrator/providers/openai-ws-terminal.mjs +18 -11
  108. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +31 -20
  109. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +581 -0
  110. package/src/runtime/agent/orchestrator/providers/retry-backoff.mjs +46 -7
  111. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +118 -219
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -0
  113. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +148 -45
  114. package/src/runtime/agent/orchestrator/providers/retry-midstream.mjs +81 -16
  115. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -0
  116. package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +0 -18
  117. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +57 -28
  118. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +19 -16
  119. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -97
  120. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +3 -1
  121. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +102 -18
  123. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +3 -1
  124. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +30 -2
  125. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +11 -2
  126. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +41 -21
  127. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +5 -10
  128. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +18 -7
  130. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +155 -29
  131. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +6 -3
  132. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +16 -0
  133. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +22 -10
  134. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +5 -0
  136. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +3 -0
  137. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +20 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
  139. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +12 -15
  140. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  141. package/src/runtime/memory/lib/pg/process.mjs +34 -24
  142. package/src/runtime/memory/lib/pg/supervisor.mjs +16 -13
  143. package/src/runtime/office/README.md +1 -1
  144. package/src/runtime/office/shared/values.mjs +0 -5
  145. package/src/runtime/shared/background-tasks.mjs +92 -14
  146. package/src/runtime/shared/err-text.mjs +25 -0
  147. package/src/runtime/shared/error-presentation.mjs +159 -19
  148. package/src/runtime/shared/llm/anthropic-betas.mjs +4 -3
  149. package/src/runtime/shared/llm/anthropic-effort.mjs +123 -134
  150. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +50 -14
  151. package/src/runtime/shared/llm/model-catalog.mjs +53 -40
  152. package/src/runtime/shared/llm/openai-daybreak.mjs +4 -16
  153. package/src/runtime/shared/llm/provider-catalog-registry.mjs +11 -0
  154. package/src/runtime/shared/llm/provider-model-identities.mjs +13 -4
  155. package/src/runtime/shared/llm/usage-rollup.mjs +0 -1
  156. package/src/runtime/shared/model-display.mjs +13 -0
  157. package/src/runtime/shared/quota-resume-store.mjs +11 -0
  158. package/src/runtime/shared/session-baseline-store.mjs +960 -0
  159. package/src/runtime/shared/statusline/statusline-format.mjs +4 -4
  160. package/src/runtime/shared/task-notification-envelope.mjs +2 -2
  161. package/src/runtime/shared/turn-changes.mjs +447 -219
  162. package/src/runtime/shared/turn-snapshot-store.mjs +182 -1
  163. package/src/runtime/shared/turn-snapshot.mjs +170 -48
  164. package/src/runtime/shared/turn-worktree-snapshot.mjs +135 -46
  165. package/src/runtime/shared/worktrees/contract.mjs +0 -24
  166. package/src/runtime/shared/worktrees/test-support.mjs +19 -4
  167. package/src/session-runtime/boot/apis.mjs +6 -0
  168. package/src/session-runtime/boot/infrastructure.mjs +6 -0
  169. package/src/session-runtime/lifecycle/session-catalog.mjs +2 -0
  170. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  171. package/src/session-runtime/mcp-glue/connect.mjs +29 -9
  172. package/src/session-runtime/mcp-glue/server-catalog.mjs +20 -3
  173. package/src/session-runtime/mcp-glue.mjs +3 -0
  174. package/src/session-runtime/mcp-oauth-login.mjs +174 -0
  175. package/src/session-runtime/model-route/model-switch-target.mjs +34 -0
  176. package/src/session-runtime/model-route/set-route.mjs +83 -15
  177. package/src/session-runtime/model-route/web-search-route.mjs +1 -1
  178. package/src/session-runtime/native-web-search/routes.mjs +22 -3
  179. package/src/session-runtime/provider-models/model-meta.mjs +12 -3
  180. package/src/session-runtime/quick-model-rows/web-search-rows.mjs +13 -8
  181. package/src/session-runtime/quick-web-search-models.mjs +13 -38
  182. package/src/session-runtime/resource-extension-sync.mjs +4 -0
  183. package/src/session-runtime/resource-mcp-api.mjs +98 -2
  184. package/src/session-runtime/runtime-review-api.mjs +13 -0
  185. package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +13 -1
  186. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +7 -2
  187. package/src/session-runtime/services/agent-tool/send-flow.mjs +22 -4
  188. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +2 -0
  189. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +22 -5
  190. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -1
  191. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -2
  192. package/src/session-runtime/services/agent-tool/worker-rows.mjs +17 -0
  193. package/src/session-runtime/settings-compaction-api.mjs +1 -0
  194. package/src/standalone/daemon-drain-policy.mjs +8 -0
  195. package/src/standalone/daemon-telemetry.mjs +55 -0
  196. package/src/standalone/daemon.mjs +3 -7
  197. package/src/standalone/session-protocol.mjs +7 -0
  198. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +33 -2
  199. package/src/standalone/session-service/agent-tree/agent-turns.mjs +26 -10
  200. package/src/standalone/session-service/agent-tree.mjs +12 -0
  201. package/src/standalone/session-service/quota-resume.mjs +8 -0
  202. package/src/standalone/session-service/retention/busy.mjs +15 -1
  203. package/src/standalone/session-service-api.mjs +5 -0
  204. package/src/standalone/session-service.mjs +1 -0
  205. package/src/tui/app/app-view.jsx +1 -0
  206. package/src/tui/app/create-app-pickers.mjs +4 -0
  207. package/src/tui/app/extension-pickers/mcp-login.mjs +107 -0
  208. package/src/tui/app/extension-pickers/mcp-servers-picker.mjs +2 -1
  209. package/src/tui/app/extension-pickers.mjs +2 -1
  210. package/src/tui/app/model-options.mjs +11 -16
  211. package/src/tui/app/slash-commands.mjs +7 -2
  212. package/src/tui/app/slash-dispatch/panel-commands.mjs +16 -1
  213. package/src/tui/components/StatusLine.jsx +2 -0
  214. package/src/tui/components/status-line/local-line.mjs +5 -2
  215. package/src/tui/components/status-line/use-statusline-text.mjs +7 -1
  216. package/src/tui/dist/index.mjs +816 -43
  217. package/src/tui/session/quota-resume.mjs +46 -14
  218. package/src/tui/session/session-api/commands.mjs +33 -24
  219. package/src/tui/session/session-api/extensions.mjs +22 -0
  220. package/src/tui/session/session-api/integrations.mjs +7 -0
  221. package/src/tui/session/session-api/lifecycle.mjs +5 -1
  222. package/src/tui/session/session-api/routes.mjs +71 -7
  223. package/src/tui/session/session-api/settings.mjs +5 -14
  224. package/src/tui/session/session-flow/drain.mjs +7 -1
  225. package/src/tui/session/session-flow/failed-turn-rewind.mjs +10 -4
  226. package/src/tui/session/turn.mjs +38 -15
  227. package/src/tui/statusline-ansi-bridge.mjs +1 -1
  228. package/src/ui/statusline-agents.mjs +15 -4
  229. package/src/ui/statusline.mjs +7 -3
  230. package/src/ui/streaming-markdown-heal.mjs +34 -3
  231. package/src/vendor/statusline/src/gateway/claude-current.mjs +14 -4
  232. package/src/runtime/memory/lib/memory-session-merge.mjs +0 -32
  233. package/src/runtime/shared/llm/usage-session-history.mjs +0 -266
@@ -7,8 +7,20 @@ import {
7
7
  mergeMcpServerConfig,
8
8
  MCP_TIMEOUT_FIELD_GROUPS,
9
9
  } from '../runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs';
10
+ import { resolveMcpOAuthUrl } from '../runtime/agent/orchestrator/mcp/client-config.mjs';
10
11
  import { withAdoptedCopies, withPinnedCollisionNames, withPluginServerIntent } from './mcp-glue/plugin-servers.mjs';
11
12
  import { findRegisteredPlugin } from './resource-plugins-api.mjs';
13
+ import { createMcpOAuthLogins } from './mcp-oauth-login.mjs';
14
+
15
+ /** The URL a server's OAuth credentials are stored under, or null. */
16
+ function oauthUrlOf(cfg) {
17
+ try {
18
+ return resolveMcpOAuthUrl(cfg);
19
+ } catch {
20
+ // No valid remote URL: such a server never logged in.
21
+ return null;
22
+ }
23
+ }
12
24
 
13
25
  /** The `mcpServers` map of a config as a fresh object (never the live one). */
14
26
  function mcpServersOf(config) {
@@ -59,14 +71,45 @@ export function createMcpResourceApi({ deps, sync, decorate }) {
59
71
  mcpStatus,
60
72
  getMcpServerConfig,
61
73
  connectConfiguredMcp,
74
+ clearMcpServerOAuth,
62
75
  reloadFullConfig,
63
76
  awaitKeychainPrewarm,
64
77
  pluginsStatus,
65
- resolveMcpCatalog = () => ({ adoption: null, plugins: {}, entries: {} }),
78
+ resolveMcpCatalog = () => ({ servers: {}, adoption: null, plugins: {}, entries: {} }),
79
+ resolveConfiguredMcpCatalog = resolveMcpCatalog,
66
80
  pluginMcpServer = () => null,
67
81
  flushSkillsSave,
82
+ cancelMcpLogin = () => {},
83
+ completeMcpLogin,
84
+ getMcpServerOAuthState = () => null,
85
+ probeMcpServerAuth,
68
86
  } = deps;
69
87
 
88
+ // Explicit OAuth sign-in: the one interactive MCP connect. On success the
89
+ // other runtimes reconnect the server non-interactively with the new tokens.
90
+ const oauthLogins = createMcpOAuthLogins({
91
+ async connect(name, openBrowser) {
92
+ const status = await connectConfiguredMcp({ only: name, enabled: true, login: { openBrowser } });
93
+ const row = status?.servers?.find((server) => server.name === name);
94
+ return { connected: row?.connected === true, error: row?.error || null };
95
+ },
96
+ cancel: (name) => cancelMcpLogin(name),
97
+ completeCallback: completeMcpLogin,
98
+ onComplete: (name) => sync.announce('mcp', { detail: { mcpChanges: [{ name, enabled: true }] } }),
99
+ });
100
+ /** The effective config of a server that signs in with MCP OAuth (HTTP/SSE
101
+ * without configured authentication). */
102
+ const oauthServerConfig = async (name) => {
103
+ const serverName = clean(name);
104
+ if (!serverName) throw new Error('MCP server name is required');
105
+ await awaitKeychainPrewarm();
106
+ reloadFullConfig();
107
+ const cfg = resolveMcpCatalog().servers[serverName];
108
+ if (!cfg) throw new Error(`MCP server not configured: ${serverName}`);
109
+ if (!getMcpServerOAuthState(cfg)) throw new Error(`MCP server "${serverName}" does not use OAuth sign-in`);
110
+ return { serverName, cfg };
111
+ };
112
+
70
113
  /** The config with any adoptable plugin server copies already adopted, so
71
114
  * every write also persists the adoption and never re-adds a copy. */
72
115
  const adoptedConfig = () => withAdoptedCopies(getConfig(), resolveMcpCatalog().adoption);
@@ -115,6 +158,49 @@ export function createMcpResourceApi({ deps, sync, decorate }) {
115
158
  const { enabled } = getMcpServerConfig(serverName);
116
159
  return sync.reconnectMcpAndAnnounce({ only: serverName, enabled });
117
160
  },
161
+ /** `{ flowId, state: 'pending', url }` once the browser login starts
162
+ * (`options.openBrowser === false` leaves opening `url` to the caller), or
163
+ * `{ flowId, state: 'complete' }` when stored credentials sufficed. */
164
+ async beginMcpOAuthLogin(name, options = {}) {
165
+ const { serverName, cfg } = await oauthServerConfig(name);
166
+ if (cfg.enabled === false) throw new Error(`MCP server is disabled: ${serverName}`);
167
+ return oauthLogins.begin(serverName, options);
168
+ },
169
+ /** `{ flowId, state: 'pending' | 'complete' | 'failed' | 'cancelled', error? }` */
170
+ getMcpOAuthLoginStatus(flowId) {
171
+ return oauthLogins.status(flowId);
172
+ },
173
+ cancelMcpOAuthLogin(flowId) {
174
+ return oauthLogins.cancel(flowId);
175
+ },
176
+ /** Remote / headless sign-in: complete the pending flow with the redirect
177
+ * URL pasted from the browser. `{ flowId, state: 'complete' | 'failed',
178
+ * error? }`; an invalid paste throws a fixed message and stays pending. */
179
+ completeMcpOAuthLogin(flowId, callbackUrl) {
180
+ return oauthLogins.complete(flowId, callbackUrl);
181
+ },
182
+ /** Forget the server's stored OAuth credentials (shared by every server
183
+ * with the same exact URL) and reconnect without a login, here and on
184
+ * peers, so it reports sign-in required. */
185
+ async signOutMcpServer(name) {
186
+ const { serverName, cfg } = await oauthServerConfig(name);
187
+ // A begin issued meanwhile waits until the credentials are gone.
188
+ return oauthLogins.cancelServer(serverName, async () => {
189
+ await clearMcpServerOAuth(cfg);
190
+ return sync.reconnectMcpAndAnnounce({ only: serverName, enabled: cfg.enabled !== false });
191
+ });
192
+ },
193
+ /** `{ auth: 'oauth' | 'none' | 'configured' | 'unreachable', error? }` for an
194
+ * HTTP/SSE config, without credentials or a browser. */
195
+ probeMcpServerAuth(cfg) {
196
+ if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) throw new TypeError('MCP server config is required');
197
+ const probe = {};
198
+ for (const key of ['type', 'url', 'headers', 'bearer_token_env_var', 'env_http_headers']) {
199
+ if (cfg[key] !== undefined) probe[key] = cfg[key];
200
+ }
201
+ if (probe.type !== 'http' && probe.type !== 'sse') throw new TypeError("type must be 'http' or 'sse'");
202
+ return probeMcpServerAuth(probe);
203
+ },
118
204
  async addMcpServer(input = {}) {
119
205
  const { name, config: serverConfig } = normalizeMcpServerInput(input);
120
206
  const servers = userServers();
@@ -162,9 +248,19 @@ export function createMcpResourceApi({ deps, sync, decorate }) {
162
248
  refusePluginServer(serverName);
163
249
  throw new Error(`MCP server not configured: ${serverName}`);
164
250
  }
251
+ const removed = servers[serverName];
165
252
  delete servers[serverName];
166
253
  saveServers(servers);
167
- return sync.reconnectMcpAndAnnounce({ drop: [serverName] });
254
+ const status = await sync.reconnectMcpAndAnnounce({ drop: [serverName] });
255
+ // A removed server keeps no OAuth tokens or client registration, unless
256
+ // another configured server (plugin or disabled ones included, even
257
+ // with MCP disabled for this runtime) still uses them through the same
258
+ // exact URL.
259
+ const url = oauthUrlOf(removed);
260
+ if (url && !Object.values(resolveConfiguredMcpCatalog().servers).some((cfg) => oauthUrlOf(cfg) === url)) {
261
+ await clearMcpServerOAuth(removed);
262
+ }
263
+ return status;
168
264
  },
169
265
  async setMcpServerEnabled(name, enabled) {
170
266
  const serverName = clean(name);
@@ -1,6 +1,7 @@
1
1
  import { registerCacheInvalidationListener } from '../runtime/agent/orchestrator/tools/builtin/cache-layers.mjs';
2
2
  import { turnReviewReply } from '../runtime/shared/turn-review-reply.mjs';
3
3
  import {
4
+ clearSessionChangeHistory,
4
5
  getSessionReviewDiff,
5
6
  getTurnChanges,
6
7
  getTurnReviewDiff,
@@ -10,6 +11,17 @@ import {
10
11
  revertTurnReviewFile,
11
12
  } from '../runtime/shared/turn-snapshot.mjs';
12
13
  import { noteTurnWorktreeChange } from '../runtime/shared/turn-worktree-snapshot.mjs';
14
+ import { forgetSessionChangeHistoryMarker } from '../runtime/shared/turn-snapshot-store.mjs';
15
+ import { forgetSessionBaselines } from '../runtime/shared/session-baseline-store.mjs';
16
+ import { registerSessionPurgeHook } from '../runtime/agent/orchestrator/session/store.mjs';
17
+
18
+ // A deleted history stays deleted for as long as its session exists; the
19
+ // session's own deletion takes the marker — and its stored first-seen file
20
+ // contents — with it.
21
+ registerSessionPurgeHook((sessionId) => {
22
+ void forgetSessionChangeHistoryMarker(sessionId);
23
+ void forgetSessionBaselines(sessionId);
24
+ });
13
25
 
14
26
  // Tool writes (apply_patch, mutating shell commands, git, watcher events)
15
27
  // reach the live review snapshot as its change signal: a review poll right
@@ -31,5 +43,6 @@ export function createRuntimeReviewApi({ getCwd, getSessionId }) {
31
43
  revertTurnChanges(getSessionId(), checkpointId, file, revision),
32
44
  reapplyTurnChanges: (checkpointId, file, revision) =>
33
45
  reapplyTurnChanges(getSessionId(), checkpointId, file, revision),
46
+ clearSessionChangeHistory: () => clearSessionChangeHistory(getSessionId()),
34
47
  };
35
48
  }
@@ -19,7 +19,7 @@
19
19
  // Every call is idempotent (the first terminal state wins, so the outer reject
20
20
  // path cannot double-notify) and absorbs its own errors, so reporting on a
21
21
  // turn can never break the turn.
22
- import { reconcileBackgroundTask } from '../../../runtime/shared/background-tasks.mjs';
22
+ import { reconcileBackgroundTask, settleCancelledBackgroundTask } from '../../../runtime/shared/background-tasks.mjs';
23
23
 
24
24
  function reconcileJobTerminalResult(job, value) {
25
25
  if (!job?.taskId) return;
@@ -50,6 +50,8 @@ export function terminalResultHook({ job, turnReview, completionValue, notifyEar
50
50
  turnReview.complete();
51
51
  const value = completionValue(terminalResult);
52
52
  job._terminalResultValue = value;
53
+ // A cancelled job reports once, as `cancelled`, when its turn settles.
54
+ if (job.status === 'cancelled') return;
53
55
  notifyEarly(job, value, notifyContext || {});
54
56
  reconcileJobTerminalResult(job, value);
55
57
  };
@@ -67,6 +69,16 @@ export function reconcileJobWatchdogPartial(job, value) {
67
69
  } catch {}
68
70
  }
69
71
 
72
+ /** The cancelled turn stopped: deliver the owner's single `cancelled`
73
+ * notification, carrying the output the turn produced before it stopped
74
+ * (header-only when it produced none). */
75
+ export function reconcileJobCancelled(job, value) {
76
+ if (!job?.taskId) return;
77
+ try {
78
+ settleCancelledBackgroundTask(job.taskId, value?.content ? { result: value } : {});
79
+ } catch {}
80
+ }
81
+
70
82
  export function reconcileJobStreamStalled(job, error) {
71
83
  if (!job?.taskId || job._terminalResultValue !== undefined) return;
72
84
  try {
@@ -5,6 +5,8 @@ import { ACTIVE_STAGES } from '../tool-def.mjs';
5
5
  import { isTerminalWorkerStatus } from '../worker-rows.mjs';
6
6
  import { sessionUsageTotals } from '../../../../runtime/shared/session-usage-totals.mjs';
7
7
 
8
+ const CANCELLED_WORKER_STATUS = /^(?:cancelled|canceled|killed|cancel[-_\s]?(?:unconfirmed|pending))$/;
9
+
8
10
  // Idle, closed, errored and reaped/unknown rows are history and stay reachable
9
11
  // through the task section (status/read keep their terminal result).
10
12
  export function isActiveWorkerRow(row = {}) {
@@ -14,8 +16,11 @@ export function isActiveWorkerRow(row = {}) {
14
16
  const status = String(row.status ?? '')
15
17
  .trim()
16
18
  .toLowerCase();
17
- if (ACTIVE_STAGES.has(stage) || ACTIVE_STAGES.has(status)) return true;
18
- const effective = stage || status;
19
+ // `status` decides; the stage is the runtime phase and only stands in for a
20
+ // row with no status. A cancel in either field is not work.
21
+ if (CANCELLED_WORKER_STATUS.test(stage) || CANCELLED_WORKER_STATUS.test(status)) return false;
22
+ const effective = status || stage;
23
+ if (ACTIVE_STAGES.has(effective)) return true;
19
24
  if (!effective || effective === 'unknown') return false;
20
25
  return !isTerminalWorkerStatus(effective);
21
26
  }
@@ -1,12 +1,14 @@
1
1
  import {
2
2
  resolveAgentWatchdogPolicy,
3
3
  resolveHandoffMessageStartIndex,
4
+ turnPartialHandoff,
4
5
  watchdogPartialHandoffFromError,
5
6
  watchdogStoppedHandoff,
6
7
  } from '../../../runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs';
7
8
  import { clean, normalizeAgentName, resolvePrompt } from './helpers.mjs';
8
9
  import { abnormalEmptyFinishError, renderResult } from './render.mjs';
9
10
  import {
11
+ reconcileJobCancelled,
10
12
  reconcileJobFinally,
11
13
  reconcileJobStreamStalled,
12
14
  reconcileJobWatchdogPartial,
@@ -93,6 +95,18 @@ export function createSendFlow({ mgr, defaultCwd, sessionSurface, canUseSessionS
93
95
  turnStartedAt: new Date().toISOString(),
94
96
  });
95
97
  let handoffMsgStart = 0;
98
+ // A cancelled turn reports the output it produced before it stopped.
99
+ const settleCancelled = (error, result = null) => {
100
+ finalStatus = 'cancelled';
101
+ reconcileJobCancelled(job, {
102
+ tag,
103
+ sessionId,
104
+ agent: session.agent || null,
105
+ provider: session.provider,
106
+ model: session.model,
107
+ content: turnPartialHandoff(error, mgr.getSession(sessionId), handoffMsgStart) || result?.content || '',
108
+ });
109
+ };
96
110
  try {
97
111
  handoffMsgStart = resolveHandoffMessageStartIndex(mgr.getSession(sessionId));
98
112
  const turnHooks = buildTurnHooks({ sessionId, notifyContext, job, turnReview, completionValue });
@@ -100,6 +114,10 @@ export function createSendFlow({ mgr, defaultCwd, sessionSurface, canUseSessionS
100
114
  // Early preview no longer suppresses the canonical body notification;
101
115
  // notifyTaskCompletion fires once with output via resolve/reconcile.
102
116
  const finalValue = completionValue(result);
117
+ if (job?.status === 'cancelled') {
118
+ settleCancelled(null, result);
119
+ return finalValue;
120
+ }
103
121
  if (finalValue.error) {
104
122
  finalStatus = 'error';
105
123
  if (job) job._terminalResultValue = finalValue;
@@ -107,6 +125,10 @@ export function createSendFlow({ mgr, defaultCwd, sessionSurface, canUseSessionS
107
125
  }
108
126
  return finalValue;
109
127
  } catch (error) {
128
+ if (job?.status === 'cancelled') {
129
+ settleCancelled(error);
130
+ throw error;
131
+ }
110
132
  const partial = watchdogPartialHandoffFromError(error, mgr.getSession(sessionId), handoffMsgStart);
111
133
  if (partial) {
112
134
  finalStatus = 'idle';
@@ -123,10 +145,6 @@ export function createSendFlow({ mgr, defaultCwd, sessionSurface, canUseSessionS
123
145
  reconcileJobWatchdogPartial(job, value);
124
146
  return value;
125
147
  }
126
- if (job?.status === 'cancelled') {
127
- finalStatus = 'cancelled';
128
- throw error;
129
- }
130
148
  finalStatus = 'error';
131
149
  reconcileJobStreamStalled(job, error);
132
150
  throw error;
@@ -44,6 +44,8 @@ export function createJobStarter({ mgr, notifyStatusChange, notifySessionComplet
44
44
  meta: jobMeta,
45
45
  resultType: 'agent_task_result',
46
46
  renderResult: (result) => renderResult(result),
47
+ // Cancellation reaches the owner once, with the worker's partial output.
48
+ notifyOnCancel: true,
47
49
  cancel: () => {
48
50
  try {
49
51
  admissionController.abort(new Error('agent task cancelled before resource admission'));
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { createNotify } from '../notify.mjs';
8
8
  import {
9
+ reconcileJobCancelled,
9
10
  reconcileJobFinally,
10
11
  reconcileJobStreamStalled,
11
12
  reconcileJobWatchdogPartial,
@@ -15,11 +16,13 @@ import { abnormalEmptyFinishError } from '../render.mjs';
15
16
  import { createProgressWatchdogRegistry } from '../../agent-watchdog-registry.mjs';
16
17
  import {
17
18
  resolveHandoffMessageStartIndex,
19
+ turnPartialHandoff,
18
20
  watchdogPartialHandoffFromError,
19
21
  watchdogStoppedHandoff,
20
22
  } from '../../../../runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs';
21
23
  import { saveSessionAsync } from '../../../../runtime/agent/orchestrator/session/store.mjs';
22
24
  import { presetDescriptor } from './spawn-prep.mjs';
25
+ import { workerStageForStatus } from '../worker-rows.mjs';
23
26
 
24
27
  // Mid-turn worker persistence cadence. The watchdog sweep runs every second;
25
28
  // a transcript save is far more expensive than a bookkeeping stamp, so the
@@ -68,7 +71,7 @@ export function createSpawnRunner({
68
71
  const messages = Number(progress?.messages) || 0;
69
72
  upsertWorkerSessionDeferred(session, tag, {
70
73
  status: 'running',
71
- stage: progress.stage || 'running',
74
+ stage: workerStageForStatus('running', progress.stage),
72
75
  ...(messages > 0 ? { messages } : {}),
73
76
  });
74
77
  // The canonical runtime owns persistence; this session is only its descriptor.
@@ -135,6 +138,16 @@ export function createSpawnRunner({
135
138
  });
136
139
  notifyStatusChange();
137
140
  let handoffMsgStart = 0;
141
+ // A cancelled turn reports the output it produced before it stopped.
142
+ const settleCancelled = (error, result = null) => {
143
+ finalStatus = 'cancelled';
144
+ reconcileJobCancelled(job, {
145
+ tag,
146
+ sessionId: session.id,
147
+ ...presetDescriptor(agent, preset, presetName),
148
+ content: turnPartialHandoff(error, mgr.getSession(session.id), handoffMsgStart) || result?.content || '',
149
+ });
150
+ };
138
151
  try {
139
152
  handoffMsgStart = resolveHandoffMessageStartIndex(mgr.getSession(session.id));
140
153
  const turnHooks = {
@@ -157,6 +170,10 @@ export function createSpawnRunner({
157
170
  // notifyTaskCompletion is left to fire exactly once with output via the
158
171
  // resolve/reconcile/finally path.
159
172
  const finalValue = completionValue(result);
173
+ if (job?.status === 'cancelled') {
174
+ settleCancelled(null, result);
175
+ return finalValue;
176
+ }
160
177
  // Non-job return path (or job path where the terminal-result reconcile
161
178
  // already ran): if the finish was abnormal-empty, surface it as a thrown
162
179
  // error so finalStatus becomes 'error' and the caller's error path (and
@@ -168,6 +185,10 @@ export function createSpawnRunner({
168
185
  }
169
186
  return finalValue;
170
187
  } catch (error) {
188
+ if (job?.status === 'cancelled') {
189
+ settleCancelled(error);
190
+ throw error;
191
+ }
171
192
  const partial = watchdogPartialHandoffFromError(error, mgr.getSession(session.id), handoffMsgStart);
172
193
  if (partial) {
173
194
  finalStatus = 'idle';
@@ -182,10 +203,6 @@ export function createSpawnRunner({
182
203
  reconcileJobWatchdogPartial(job, value);
183
204
  return value;
184
205
  }
185
- if (job?.status === 'cancelled') {
186
- finalStatus = 'cancelled';
187
- throw error;
188
- }
189
206
  finalStatus = 'error';
190
207
  reconcileJobStreamStalled(job, error);
191
208
  throw error;
@@ -11,8 +11,15 @@ const ACTIVE_WORKER_STATUS =
11
11
  /^(?:connecting|requesting|streaming|tool[-_\s]?running|running|queued|pending|starting|cancelling)$/i;
12
12
  const WORKER_POOL_FRESH_MS = 2 * 60 * 1000;
13
13
 
14
+ const CANCELLED_WORKER_STATUS = /^(?:cancelled|canceled|killed|cancel[-_\s]?(?:unconfirmed|pending))$/i;
15
+
16
+ /** `status` decides; `stage` is the runtime phase copied in by the progress
17
+ * sweep and only decides for a row with no status. A cancel in either field
18
+ * is not work (bare `cancelling` still is). */
14
19
  export const isActiveWorkerRow = (row) =>
15
- ACTIVE_WORKER_STATUS.test(clean(row?.status)) || ACTIVE_WORKER_STATUS.test(clean(row?.stage));
20
+ !CANCELLED_WORKER_STATUS.test(clean(row?.status)) &&
21
+ !CANCELLED_WORKER_STATUS.test(clean(row?.stage)) &&
22
+ ACTIVE_WORKER_STATUS.test(clean(row?.status) || clean(row?.stage));
16
23
 
17
24
  /** A persisted approval wait whose runtime is gone: the in-memory wait died
18
25
  * with that process (daemon restart), so the flag must not stick. */
@@ -2,7 +2,7 @@
2
2
  // The one row shape of the worker index: coercing whatever a stored file or a
3
3
  // caller hands in, and projecting a live session into a row.
4
4
  import { agentTagOf, clean, positiveInt } from '../helpers.mjs';
5
- import { isLeadPoolAgent } from '../worker-rows.mjs';
5
+ import { isLeadPoolAgent, workerStageForStatus } from '../worker-rows.mjs';
6
6
 
7
7
  /** An explicit boolean wins; otherwise the session's own fast flag (true) or unknown. */
8
8
  function fastFlag(explicit, sessionFast) {
@@ -72,7 +72,7 @@ export function workerRowFromSession(session, fallbackTag = '', extra = {}, getS
72
72
  if (!tag || !sessionId) return null;
73
73
  const runtime = getSessionRuntime(sessionId);
74
74
  const status = clean(extra.status) || (session?.closed === true ? 'closed' : clean(session?.status) || 'idle');
75
- const stage = clean(extra.stage) || clean(runtime?.stage) || status;
75
+ const stage = clean(extra.stage) || workerStageForStatus(status, runtime?.stage);
76
76
  const nowIso = new Date().toISOString();
77
77
  return {
78
78
  tag,
@@ -1,4 +1,5 @@
1
1
  import { agentTagOf, clean, positiveInt, stampMs } from './helpers.mjs';
2
+ import { ACTIVE_STAGES } from './tool-def.mjs';
2
3
 
3
4
  import { TAG_TOMBSTONE_TTL_MS, tagTombstoneKey } from '../../../runtime/shared/agent-reap-state.mjs';
4
5
  export { TAG_TOMBSTONE_TTL_MS, tagTombstoneKey };
@@ -27,6 +28,22 @@ export function isTerminalWorkerStatus(status) {
27
28
  );
28
29
  }
29
30
 
31
+ const WORKING_WORKER_STATUS =
32
+ /^(?:connecting|requesting|streaming|tool[-_\s]?running|running|queued|pending|starting|cancelling)$/i;
33
+ const WORKING_WORKER_STAGES = new Set([...ACTIVE_STAGES, 'resource_wait']);
34
+
35
+ /** The `stage` to store beside a worker `status`, from a stage read off the
36
+ * live runtime. That stage is the runtime's own phase, so mid-work it can say
37
+ * `idle`/`done`/`error` (a fresh or just-settled runtime entry) while the
38
+ * worker is still running. Stored next to a working status it made a mixed
39
+ * row that readers classified as finished, so a working status keeps a
40
+ * working stage and falls back to the status itself. */
41
+ export function workerStageForStatus(status, runtimeStage) {
42
+ const stage = clean(runtimeStage);
43
+ if (!WORKING_WORKER_STATUS.test(clean(status))) return stage || clean(status);
44
+ return WORKING_WORKER_STAGES.has(stage.toLowerCase()) ? stage : clean(status);
45
+ }
46
+
30
47
  export function recoverTagTombstoneOwner(row, sessions) {
31
48
  if (clean(row.parentSessionId || row.ownerSessionId)) return row;
32
49
  const pid = positiveInt(row.clientHostPid);
@@ -82,6 +82,7 @@ export function createCompactionSettings({
82
82
  const next = { ...current };
83
83
  if (hasOwn(input, 'auto')) next.auto = input.auto !== false;
84
84
  if (hasOwn(input, 'enabled')) next.auto = input.enabled !== false;
85
+ if (hasOwn(input, 'onModelChange')) next.onModelChange = input.onModelChange !== false;
85
86
  // Legacy Compact type fields are intentionally ignored. There is one
86
87
  // fresh-context Compact contract for every session.
87
88
  // A new budget representation replaces the previous one. Otherwise a
@@ -1,3 +1,11 @@
1
+ /** Whether an idle GC sweep may start: no service call and no running turn
2
+ * (provider stream / tool call). Background-task-only sessions and memory
3
+ * agents do not block it — unlike drainDeferral, which guards shutdown. */
4
+ export function idleGcEligible(work) {
5
+ const { activeCalls = 0, queuedCalls = 0, turnBusySessions = 0 } = work || {};
6
+ return !(activeCalls > 0 || queuedCalls > 0 || turnBusySessions > 0);
7
+ }
8
+
1
9
  /** Whether self-shutdown must wait for in-flight work. A forced replacement
2
10
  * never waits: shutdown() aborts busy sessions, service calls and agent runs. */
3
11
  export function drainDeferral(work, { force = false } = {}) {
@@ -93,6 +93,52 @@ export function collectDaemonTelemetry({
93
93
  };
94
94
  }
95
95
 
96
+ /**
97
+ * Tracks the smallest heapUsed observed right after a major GC (mark-sweep-
98
+ * compact) — the retained-set size, which heapUsed at an arbitrary instant
99
+ * hides. Sampled per telemetry interval; no per-GC logging. Returns null when
100
+ * PerformanceObserver is unavailable.
101
+ */
102
+ export function createPostGcHeapTracker({
103
+ heapUsed = () => v8.getHeapStatistics().used_heap_size,
104
+ observe = (onGc) => {
105
+ const obs = new PerformanceObserver((list) => {
106
+ for (const entry of list.getEntries()) onGc(entry);
107
+ });
108
+ obs.observe({ entryTypes: ['gc'] });
109
+ return () => obs.disconnect();
110
+ },
111
+ } = {}) {
112
+ let min = -1;
113
+ let disconnect = null;
114
+ try {
115
+ disconnect = observe((entry) => {
116
+ // NODE_PERFORMANCE_GC_MAJOR = 4 (older Node: entry.kind); newer: entry.detail.kind.
117
+ const kind = entry?.detail?.kind ?? entry?.kind;
118
+ if (kind !== 4) return;
119
+ const used = byteCount(heapUsed());
120
+ if (used >= 0 && (min < 0 || used < min)) min = used;
121
+ });
122
+ } catch {
123
+ return null;
124
+ }
125
+ return {
126
+ /** Min post-major-GC heap since the last take; -1 when none ran. */
127
+ take() {
128
+ const value = min;
129
+ min = -1;
130
+ return value;
131
+ },
132
+ stop() {
133
+ try {
134
+ disconnect?.();
135
+ } catch {
136
+ /* already gone */
137
+ }
138
+ },
139
+ };
140
+ }
141
+
96
142
  export function formatDaemonTelemetry(record, reason = 'periodic') {
97
143
  const row = record && typeof record === 'object' ? record : collectDaemonTelemetry();
98
144
  return [
@@ -110,6 +156,8 @@ export function formatDaemonTelemetry(record, reason = 'periodic') {
110
156
  `queuedCalls=${row.queuedCalls}`,
111
157
  `busySessions=${row.busySessions}`,
112
158
  `busyMemoryAgents=${row.busyMemoryAgents}`,
159
+ // Present only when a major GC ran since the previous line.
160
+ ...(byteCount(row.postGcHeapMinBytes) >= 0 ? [`postGcHeapMinBytes=${row.postGcHeapMinBytes}`] : []),
113
161
  ].join(' ');
114
162
  }
115
163
 
@@ -123,8 +171,10 @@ export function createDaemonTelemetry({
123
171
  heapStatistics,
124
172
  setIntervalFn = setInterval,
125
173
  clearIntervalFn = clearInterval,
174
+ createGcTracker = createPostGcHeapTracker,
126
175
  } = {}) {
127
176
  let timer = null;
177
+ let gcTracker = null;
128
178
 
129
179
  function emit(reason = 'periodic') {
130
180
  try {
@@ -141,6 +191,8 @@ export function createDaemonTelemetry({
141
191
  heapStatistics,
142
192
  work,
143
193
  });
194
+ const postGc = gcTracker?.take() ?? -1;
195
+ if (postGc >= 0) record.postGcHeapMinBytes = postGc;
144
196
  log(formatDaemonTelemetry(record, reason));
145
197
  return record;
146
198
  } catch {
@@ -159,6 +211,7 @@ export function createDaemonTelemetry({
159
211
 
160
212
  function start(intervalMs = DAEMON_TELEMETRY_INTERVAL_MS) {
161
213
  if (timer) return timer;
214
+ gcTracker ??= createGcTracker();
162
215
  const delay = Number(intervalMs);
163
216
  const ms = Number.isFinite(delay) && delay > 0 ? delay : DAEMON_TELEMETRY_INTERVAL_MS;
164
217
  const handle = setIntervalFn(() => {
@@ -174,6 +227,8 @@ export function createDaemonTelemetry({
174
227
  if (!timer) return;
175
228
  clearIntervalFn(timer);
176
229
  timer = null;
230
+ gcTracker?.stop();
231
+ gcTracker = null;
177
232
  }
178
233
 
179
234
  return { emit, start, stop };
@@ -65,7 +65,7 @@ import {
65
65
  } from '../runtime/agent/orchestrator/providers/stream-json-pool.mjs';
66
66
  import { createAgentDispatchBroker } from './agent-dispatch-broker.mjs';
67
67
  import { createChannelTransport } from './channel-transport.mjs';
68
- import { drainDeferral } from './daemon-drain-policy.mjs';
68
+ import { drainDeferral, idleGcEligible } from './daemon-drain-policy.mjs';
69
69
  import { createChannelSessionRouter } from '../session-runtime/services/channel-session-router.mjs';
70
70
  import { createSessionTransport } from './session-transport.mjs';
71
71
  import { createSessionService } from './session-service.mjs';
@@ -309,6 +309,7 @@ function inFlightWork() {
309
309
  queuedCalls: (transport?.queuedCount ?? 0) + (sessionTransport?.queuedCount ?? 0),
310
310
  busySessions: sessionService?.busyCount ?? 0,
311
311
  busyMemoryAgents: agentDispatchBroker?.snapshot?.().inFlight ?? 0,
312
+ turnBusySessions: sessionService?.turnBusyCount ?? 0,
312
313
  };
313
314
  }
314
315
 
@@ -353,11 +354,6 @@ daemonTelemetry = createDaemonTelemetry({
353
354
  },
354
355
  });
355
356
 
356
- function daemonHasWorkInFlight() {
357
- const work = inFlightWork();
358
- return work.activeCalls > 0 || work.queuedCalls > 0 || work.busySessions > 0 || work.busyMemoryAgents > 0;
359
- }
360
-
361
357
  /** One process, two front doors (channels + sessions): an idle side must never
362
358
  * evict a busy one, so every self-shutdown trigger checks BOTH registries. */
363
359
  let _lastDeferredLog = { key: '', at: 0, suppressed: 0 };
@@ -823,7 +819,7 @@ async function main() {
823
819
  // Reclaim deferred garbage while nothing is in flight. V8 keeps a long-lived
824
820
  // daemon's dead transcripts resident for as long as the heap limit stays out
825
821
  // of sight; a measured sweep on this daemon returned 140MB in 98ms.
826
- idleGc = createIdleGc({ isBusy: daemonHasWorkInFlight, log });
822
+ idleGc = createIdleGc({ isBusy: () => !idleGcEligible(inFlightWork()), log });
827
823
  if (idleGc.arm()) log('idle gc armed');
828
824
 
829
825
  const channelsRuntime = createChannelsRuntimeLoader({
@@ -19,6 +19,7 @@ export const SESSION_READ_ACTIONS = Object.freeze([
19
19
  'getMediaDefault',
20
20
  'getMediaJob',
21
21
  'getMcpServerConfig',
22
+ 'getMcpOAuthLoginStatus',
22
23
  'getOnboardingStatus',
23
24
  'getOAuthProviderLoginStatus',
24
25
  'getOutputStyle',
@@ -85,16 +86,19 @@ export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
85
86
  'agentControl',
86
87
  'authenticateProvider',
87
88
  'beginOAuthProviderLogin',
89
+ 'beginMcpOAuthLogin',
88
90
  'updateProviderAccounts',
89
91
  'cacheMediaThumbnail',
90
92
  'cancelMediaJob',
91
93
  'cancelOAuthProviderLogin',
94
+ 'cancelMcpOAuthLogin',
92
95
  'cancelConversationImport',
93
96
  'checkForUpdate',
94
97
  'clear',
95
98
  'compact',
96
99
  'completeOnboarding',
97
100
  'completeOAuthProviderLogin',
101
+ 'completeMcpOAuthLogin',
98
102
  'consumeCodexRateLimitResetCredit',
99
103
  'createWorkflow',
100
104
  'deleteAgentDefinition',
@@ -118,6 +122,7 @@ export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
118
122
  'memoryControl',
119
123
  'openMediaAsset',
120
124
  'openMediaFolder',
125
+ 'probeMcpServerAuth',
121
126
  'pushNotice',
122
127
  'recall',
123
128
  'reconnectMcp',
@@ -143,6 +148,7 @@ export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
143
148
  'revertTurnReviewFile',
144
149
  'revertTurnChanges',
145
150
  'reapplyTurnChanges',
151
+ 'clearSessionChangeHistory',
146
152
  'rewindToItem',
147
153
  'runDoctor',
148
154
  'runScheduleNow',
@@ -200,6 +206,7 @@ export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
200
206
  'setWebhookEnabled',
201
207
  'setWorkflow',
202
208
  'setOrchestrationMode',
209
+ 'signOutMcpServer',
203
210
  'saveStarterGuideState',
204
211
  'skipOnboarding',
205
212
  'startConversationImport',