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
@@ -169,8 +169,30 @@ export function noteTurnWorktreeChange(paths = null) {
169
169
  }
170
170
  }
171
171
 
172
- function withStateLock(state, task) {
173
- const next = state.lock.then(task, task);
172
+ // A lock hold (queue wait plus work) at least this long is logged, so a slow
173
+ // review bar can be told apart as waiting behind other captures or slow git.
174
+ const SLOW_LOCK_LOG_MS = 1_000;
175
+
176
+ function withStateLock(state, task, op = 'task') {
177
+ const queuedAt = Date.now();
178
+ const queuedBehind = state.lockDepth || 0;
179
+ state.lockDepth = queuedBehind + 1;
180
+ let startedAt = 0;
181
+ const timed = async () => {
182
+ startedAt = Date.now();
183
+ return await task();
184
+ };
185
+ const next = state.lock.then(timed, timed).finally(() => {
186
+ state.lockDepth -= 1;
187
+ const finishedAt = Date.now();
188
+ const waitMs = (startedAt || finishedAt) - queuedAt;
189
+ if (finishedAt - queuedAt >= SLOW_LOCK_LOG_MS) {
190
+ console.info(
191
+ `[turn-snapshot-lock] op=${op} waitMs=${waitMs} workMs=${finishedAt - (startedAt || finishedAt)} ` +
192
+ `queuedBehind=${queuedBehind} root=${state.root}`
193
+ );
194
+ }
195
+ });
174
196
  state.lock = next.then(
175
197
  () => undefined,
176
198
  () => undefined
@@ -338,6 +360,7 @@ async function ensureState(state) {
338
360
  }
339
361
 
340
362
  async function resetShadowIndex(state) {
363
+ state.lastCapture = null;
341
364
  state.sourceIndexPath ??= await sourceGitPath(state.root, 'index');
342
365
  const sourceIndex = state.sourceIndexPath;
343
366
  const target = join(state.gitDir, 'index');
@@ -400,10 +423,33 @@ async function changedWorktreePaths(state) {
400
423
  };
401
424
  }
402
425
 
403
- async function captureTreeUnlocked(state, toolPaths = []) {
426
+ /** The capture sequence a caller observes before queueing on the lock. */
427
+ function captureSequence(state) {
428
+ return state.captureSeq || 0;
429
+ }
430
+
431
+ /** `after` (a captureSequence() read before queueing): a capture of this root
432
+ * that began after that point, with no reported write since and covering
433
+ * every explicit path, already is a capture that began after the call. Many
434
+ * sessions of one repository queue reads together; they share that capture
435
+ * instead of each re-scanning the worktree in turn. */
436
+ async function captureTreeUnlocked(state, toolPaths = [], { after = -1 } = {}) {
404
437
  await ensureState(state);
405
- const { paths, excludedUntracked } = await changedWorktreePaths(state);
406
438
  const explicit = new Set(toolPaths);
439
+ const last = state.lastCapture;
440
+ if (
441
+ after >= 0 &&
442
+ last &&
443
+ last.seq > after &&
444
+ last.stamp === state.changeStamp &&
445
+ [...explicit].every((rel) => last.explicit.has(rel))
446
+ ) {
447
+ return last.tree;
448
+ }
449
+ state.captureSeq = captureSequence(state) + 1;
450
+ const capture = { seq: state.captureSeq, stamp: state.changeStamp, explicit, tree: '' };
451
+ state.lastCapture = null;
452
+ const { paths, excludedUntracked } = await changedWorktreePaths(state);
407
453
  for (const rel of explicit) {
408
454
  try {
409
455
  const stat = await lstat(resolve(state.root, rel));
@@ -432,7 +478,9 @@ async function captureTreeUnlocked(state, toolPaths = []) {
432
478
  });
433
479
  }
434
480
  const tree = await runGit(shadowArgs(state, ['write-tree']), { cwd: state.root });
435
- return clean(tree.stdout);
481
+ capture.tree = clean(tree.stdout);
482
+ state.lastCapture = capture;
483
+ return capture.tree;
436
484
  }
437
485
 
438
486
  /** Only explicit tool targets bypass ignore/size filters. Their first-before
@@ -875,39 +923,74 @@ export async function resumeTurnWorktreeSnapshot(
875
923
  const root = await repositoryRoot(worktree).catch(() => null);
876
924
  if (!root) return null;
877
925
  const state = stateForRoot(root);
878
- return await withStateLock(state, async () => {
879
- await ensureState(state);
880
- // A collected or never-written baseline restores nothing. Report that as
881
- // "no snapshot" instead of failing every later call made against it.
882
- const kind = await runGit(shadowArgs(state, ['cat-file', '-t', tree]), {
926
+ const after = captureSequence(state);
927
+ return await withStateLock(
928
+ state,
929
+ async () => {
930
+ await ensureState(state);
931
+ // A collected or never-written baseline restores nothing. Report that as
932
+ // "no snapshot" instead of failing every later call made against it.
933
+ const kind = await runGit(shadowArgs(state, ['cat-file', '-t', tree]), {
934
+ cwd: state.root,
935
+ allowFailure: true,
936
+ });
937
+ if (kind.code !== 0 || clean(kind.stdout) !== 'tree') return null;
938
+ const scopePaths = Array.isArray(paths) && paths.length > 0 ? [...new Set(paths)] : null;
939
+ const applied = await applyBaselineFilesUnlocked(state, tree, baselineFiles || []);
940
+ const snapshot = {
941
+ state,
942
+ root,
943
+ baselineTree: applied.tree,
944
+ currentTree: tree,
945
+ patch: '',
946
+ files: [],
947
+ patchTruncated: false,
948
+ scopePaths,
949
+ toolPaths: new Set(scopePaths || []),
950
+ pendingBaselines: new Map(),
951
+ baselineFiles: new Map(applied.files.map((entry) => [pathKey(entry.path), entry])),
952
+ };
953
+ const currentTree = await captureTreeUnlocked(state, snapshot.toolPaths, { after });
954
+ Object.assign(
955
+ snapshot,
956
+ withOmittedBaselines(
957
+ snapshot,
958
+ await diffTreesUnlocked(state, snapshot.baselineTree, currentTree, scopePaths, { omitPatch })
959
+ )
960
+ );
961
+ return snapshot;
962
+ },
963
+ 'resume'
964
+ );
965
+ }
966
+
967
+ // Git's well-known empty tree: what an unborn HEAD (no commit yet) compares as.
968
+ const EMPTY_TREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
969
+
970
+ /** The files among `paths` whose bytes in the snapshot's last capture still
971
+ * differ from the source repository's current HEAD (staged or not), with
972
+ * their counts against HEAD; no patch text. An ignored file that HEAD does
973
+ * not hold is no uncommitted change and is left out. */
974
+ export async function diffTurnWorktreeHead(snapshot, paths) {
975
+ const scope = [...new Set((paths || []).filter(Boolean))];
976
+ if (!snapshot?.state || !snapshot.currentTree || scope.length === 0) return [];
977
+ return await withStateLock(snapshot.state, async () => {
978
+ const { state } = snapshot;
979
+ const head = await runGit(['rev-parse', '--verify', '--quiet', 'HEAD^{tree}'], {
883
980
  cwd: state.root,
884
981
  allowFailure: true,
885
982
  });
886
- if (kind.code !== 0 || clean(kind.stdout) !== 'tree') return null;
887
- const scopePaths = Array.isArray(paths) && paths.length > 0 ? [...new Set(paths)] : null;
888
- const applied = await applyBaselineFilesUnlocked(state, tree, baselineFiles || []);
889
- const snapshot = {
890
- state,
891
- root,
892
- baselineTree: applied.tree,
893
- currentTree: tree,
894
- patch: '',
895
- files: [],
896
- patchTruncated: false,
897
- scopePaths,
898
- toolPaths: new Set(scopePaths || []),
899
- pendingBaselines: new Map(),
900
- baselineFiles: new Map(applied.files.map((entry) => [pathKey(entry.path), entry])),
901
- };
902
- const currentTree = await captureTreeUnlocked(state, snapshot.toolPaths);
903
- Object.assign(
904
- snapshot,
905
- withOmittedBaselines(
906
- snapshot,
907
- await diffTreesUnlocked(state, snapshot.baselineTree, currentTree, scopePaths, { omitPatch })
908
- )
909
- );
910
- return snapshot;
983
+ const headTree = head.code === 0 ? clean(head.stdout) : EMPTY_TREE;
984
+ const { files } = await diffTreesUnlocked(state, headTree, snapshot.currentTree, scope, { omitPatch: true });
985
+ const added = files.filter((file) => file.status === 'A').map((file) => file.path);
986
+ if (added.length === 0) return files;
987
+ const ignored = await runGit(['check-ignore', '-z', '--stdin'], {
988
+ cwd: state.root,
989
+ input: `${added.join('\0')}\0`,
990
+ allowFailure: true,
991
+ });
992
+ const skip = new Set(splitNulList(ignored.stdout).map(pathKey));
993
+ return files.filter((file) => file.status !== 'A' || !skip.has(pathKey(file.path)));
911
994
  });
912
995
  }
913
996
 
@@ -930,17 +1013,22 @@ function startRefresh(snapshot) {
930
1013
  const flight = { seq: (snapshot.refreshSeq || 0) + 1, promise: null };
931
1014
  snapshot.refreshSeq = flight.seq;
932
1015
  snapshot.refreshing = flight;
933
- flight.promise = withStateLock(snapshot.state, async () => {
934
- const mark = await captureMark(snapshot.state);
935
- await materializeToolBaselines(snapshot);
936
- const currentTree = await captureTreeUnlocked(snapshot.state, snapshot.toolPaths);
937
- if (currentTree !== snapshot.currentTree) {
938
- const review = await diffTreesUnlocked(snapshot.state, snapshot.baselineTree, currentTree, snapshot.scopePaths);
939
- Object.assign(snapshot, withOmittedBaselines(snapshot, review));
940
- }
941
- snapshot.captured = mark;
942
- return snapshot;
943
- }).finally(() => {
1016
+ const after = captureSequence(snapshot.state);
1017
+ flight.promise = withStateLock(
1018
+ snapshot.state,
1019
+ async () => {
1020
+ const mark = await captureMark(snapshot.state);
1021
+ await materializeToolBaselines(snapshot);
1022
+ const currentTree = await captureTreeUnlocked(snapshot.state, snapshot.toolPaths, { after });
1023
+ if (currentTree !== snapshot.currentTree) {
1024
+ const review = await diffTreesUnlocked(snapshot.state, snapshot.baselineTree, currentTree, snapshot.scopePaths);
1025
+ Object.assign(snapshot, withOmittedBaselines(snapshot, review));
1026
+ }
1027
+ snapshot.captured = mark;
1028
+ return snapshot;
1029
+ },
1030
+ 'refresh'
1031
+ ).finally(() => {
944
1032
  if (snapshot.refreshing === flight) snapshot.refreshing = null;
945
1033
  });
946
1034
  return flight.promise;
@@ -982,6 +1070,7 @@ function safeRelativePath(root, value) {
982
1070
  }
983
1071
 
984
1072
  async function restorePathFromTree(snapshot, rel) {
1073
+ snapshot.state.lastCapture = null;
985
1074
  const listed = await runGit(
986
1075
  shadowArgs(snapshot.state, ['ls-tree', '-z', '--name-only', snapshot.baselineTree, '--', rel]),
987
1076
  { cwd: snapshot.root }
@@ -19,7 +19,6 @@ export const DEFAULT_WORKTREE_BRANCH_PREFIX = 'mixdog/';
19
19
 
20
20
  export const WORKSPACE_TRANSITION_STATES = Object.freeze(['idle', 'provisioning', 'switching', 'removing', 'recovery']);
21
21
  const WORKTREE_ENTRY_STATES = Object.freeze(['creating', 'ready', 'deleting', 'recovery']);
22
- const BRANCH_OCCUPANCY_KINDS = Object.freeze(['current', 'session', 'external', 'free']);
23
22
 
24
23
  /**
25
24
  * Stable error codes. Clients branch on `error.code`, never on message text.
@@ -302,29 +301,6 @@ export function normalizeWorktreeOwner(input) {
302
301
  * @typedef {{ branch: string, kind: BranchOccupancyKind, sessionId?: string, path?: string }} BranchOccupancy
303
302
  */
304
303
 
305
- /**
306
- * @param {unknown} input
307
- * @returns {BranchOccupancy}
308
- */
309
- export function normalizeBranchOccupancy(input) {
310
- const value = record(input, 'occupancy');
311
- const branch = text(value.branch, 'occupancy.branch');
312
- const kind = oneOf(value.kind, BRANCH_OCCUPANCY_KINDS, 'occupancy.kind');
313
- if (kind === 'free') {
314
- if (value.sessionId != null || value.path != null) {
315
- throw new TypeError('A free branch carries no sessionId or path.');
316
- }
317
- return { branch, kind };
318
- }
319
- if (kind !== 'session' && value.sessionId != null) {
320
- throw new TypeError('Only a session occupancy carries a sessionId.');
321
- }
322
- const occupancy = { branch, kind };
323
- if (kind === 'session') occupancy.sessionId = text(value.sessionId, 'occupancy.sessionId');
324
- if (value.path != null) occupancy.path = text(value.path, 'occupancy.path');
325
- return occupancy;
326
- }
327
-
328
304
  /**
329
305
  * The branch menu's rows (the desktop DesktopGitBranch shape) for the local
330
306
  * branch names the manager lists: `current` marks the branch checked out in
@@ -1,6 +1,6 @@
1
1
  // Temporary git repositories and data dirs for the worktree suites.
2
2
  import { execFileSync } from 'node:child_process';
3
- import { chmodSync, mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
3
+ import { chmodSync, cpSync, existsSync, mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { dirname, join } from 'node:path';
6
6
 
@@ -56,14 +56,29 @@ export function commitFile(cwd, rel, content, message = `edit ${rel}`) {
56
56
  return git(cwd, 'rev-parse', 'HEAD');
57
57
  }
58
58
 
59
- /** A repository on `main` with one commit. */
60
- export function makeRepo({ commit = true } = {}) {
61
- const dir = tempDir('mixdog-wt-repo-');
59
+ // Built once per process and copied per repository: the five or six git
60
+ // spawns behind one fresh repository cost ~0.8 s on win32, and the worktree
61
+ // suites make dozens. A non-bare repository holds no absolute paths, so a copy
62
+ // is the same repository at its new location.
63
+ const templates = new Map();
64
+
65
+ function repoTemplate(commit) {
66
+ const existing = templates.get(commit);
67
+ if (existing && existsSync(existing)) return existing;
68
+ const dir = tempDir('mixdog-wt-template-');
62
69
  git(dir, 'init', '-q', '-b', 'main');
63
70
  git(dir, 'config', 'user.name', 'Mixdog Test');
64
71
  git(dir, 'config', 'user.email', 'mixdog@example.com');
65
72
  git(dir, 'config', 'core.autocrlf', 'false');
66
73
  if (commit) commitFile(dir, 'README.md', 'hello\n', 'initial');
74
+ templates.set(commit, dir);
75
+ return dir;
76
+ }
77
+
78
+ /** A repository on `main` with one commit. */
79
+ export function makeRepo({ commit = true } = {}) {
80
+ const dir = tempDir('mixdog-wt-repo-');
81
+ cpSync(repoTemplate(commit), dir, { recursive: true });
67
82
  return dir;
68
83
  }
69
84
 
@@ -451,12 +451,18 @@ function resourceApiFor(boot) {
451
451
  STANDALONE_DATA_DIR,
452
452
  saveConfigAndAdopt: boot.saveConfigAndAdopt,
453
453
  connectConfiguredMcp: boot.connectConfiguredMcp,
454
+ cancelMcpLogin: boot.cancelMcpLogin,
455
+ completeMcpLogin: boot.completeMcpLogin,
456
+ clearMcpServerOAuth: (cfg) => boot.mcpClient.clearMcpServerOAuth(cfg),
457
+ getMcpServerOAuthState: (cfg) => boot.mcpClient.getMcpServerOAuthState(cfg),
458
+ probeMcpServerAuth: (cfg) => boot.mcpClient.probeMcpServerAuth(cfg),
454
459
  invalidatePreSessionToolSurface: boot.invalidatePreSessionToolSurface,
455
460
  refreshEmptySessionToolPolicy: boot.refreshEmptySessionToolPolicy,
456
461
  normalizeMcpServerInput: boot.normalizeMcpServerInput,
457
462
  mcpStatus: boot.mcpStatus,
458
463
  getMcpServerConfig: boot.getMcpServerConfig,
459
464
  resolveMcpCatalog: boot.resolveMcpCatalog,
465
+ resolveConfiguredMcpCatalog: boot.resolveConfiguredMcpCatalog,
460
466
  pluginMcpServer: boot.pluginMcpServer,
461
467
  skillsStatus: boot.skillsStatus,
462
468
  skillContent: boot.skillContent,
@@ -40,11 +40,14 @@ function wireMcp(boot) {
40
40
  const {
41
41
  mcpStatus,
42
42
  resolveMcpCatalog,
43
+ resolveConfiguredMcpCatalog,
43
44
  pluginMcpServer,
44
45
  pluginMcpSummary,
45
46
  getMcpServerConfig,
46
47
  connectConfiguredMcp,
47
48
  awaitInitialMcpConnect,
49
+ cancelMcpLogin,
50
+ completeMcpLogin,
48
51
  normalizeMcpServerInput,
49
52
  } = createMcpGlue({
50
53
  mcpClient,
@@ -59,11 +62,14 @@ function wireMcp(boot) {
59
62
  Object.assign(boot, {
60
63
  mcpStatus,
61
64
  resolveMcpCatalog,
65
+ resolveConfiguredMcpCatalog,
62
66
  pluginMcpServer,
63
67
  pluginMcpSummary,
64
68
  getMcpServerConfig,
65
69
  connectConfiguredMcp,
66
70
  awaitInitialMcpConnect,
71
+ cancelMcpLogin,
72
+ completeMcpLogin,
67
73
  normalizeMcpServerInput,
68
74
  });
69
75
  }
@@ -93,6 +93,8 @@ function leadSessionRow(s, heartbeatMtimes) {
93
93
  // sessions under the sidebar Automations section instead of Recent.
94
94
  sourceType: sourceType || null,
95
95
  sourceName: clean(s.sourceName || '') || null,
96
+ createdAt: Number(s.createdAt) || 0,
97
+ inheritedFromSessionId: clean(s.inheritedFromSessionId || '') || null,
96
98
  };
97
99
  }
98
100
 
@@ -178,7 +178,7 @@ export function createTeardown(deps, { ingestSessionIntoMemory, closeSurfaceSess
178
178
  if (scope.detach) await settleDetached(withTeardownDeadline, stops);
179
179
  else await settleAll(withTeardownDeadline, stops);
180
180
  // The session went away (idle eviction included): free its save baselines.
181
- if (scope.closingSessionId) forgetSessionSaveBaseline(scope.closingSessionId);
181
+ if (scope.closingSessionId) forgetSessionSaveBaseline(scope.closingSessionId, { releaseWriteControl: true });
182
182
  // Self-update stages in the background and swaps on the next clean launch
183
183
  // (see staged-update.mjs) — nothing installs at shutdown. On a real
184
184
  // process exit we just drop this session's live-refcount pid file so a
@@ -24,11 +24,17 @@ export function createMcpConnect({
24
24
  const fingerprint = (cfg) => JSON.stringify(cfg);
25
25
  // Roots advertised by cwd-following servers track this runtime's cwd.
26
26
  const syncScopeRoot = () => mcpClient.setMcpScopeRoot?.(getMcpScopeId(), clean(getCurrentCwd?.()));
27
+ // Abort the pending interactive OAuth login of one server's connect.
28
+ const cancelMcpLogin = (name) => mcpClient.cancelMcpLogin?.(clean(name), scopeOptions());
29
+ // Answer it with the redirect URL pasted from the browser.
30
+ const completeMcpLogin = (name, callbackUrl) => mcpClient.completeMcpLogin(clean(name), callbackUrl, scopeOptions());
27
31
 
28
32
  // Connect/disconnect exactly one server in the live registry, leaving all
29
33
  // others untouched. Used by the enable/disable toggle so a single-server
30
- // change never triggers a full disconnectAll()/reconnect freeze.
31
- async function applyMcpServerConnection(name, enabled, servers = resolveEffectiveMcpServers().servers) {
34
+ // change never triggers a full disconnectAll()/reconnect freeze. Only an
35
+ // explicit sign-in passes `login` ({ openBrowser }): every other connect is
36
+ // non-interactive and reports a server that needs one as sign-in required.
37
+ async function applyMcpServerConnection(name, enabled, servers = resolveEffectiveMcpServers().servers, login = null) {
32
38
  const target = clean(name);
33
39
  if (!target) return;
34
40
  // Changing this server's state clears any stale failure record for it.
@@ -46,11 +52,16 @@ export function createMcpConnect({
46
52
  // between the close and the connect never resurrects the server.
47
53
  try {
48
54
  ensurePluginServerData(cfg);
49
- await mcpClient.reconnectSingleMcpServer(getMcpScopeId(), target, cfg);
55
+ await mcpClient.reconnectSingleMcpServer(
56
+ getMcpScopeId(),
57
+ target,
58
+ cfg,
59
+ login ? { interactive: true, openBrowser: login.openBrowser } : {}
60
+ );
50
61
  } catch (error) {
51
- const failures = Array.isArray(error?.failures)
52
- ? error.failures
53
- : [{ name: target, msg: error?.message || String(error) }];
62
+ const failure = { name: target, msg: error?.message || String(error) };
63
+ if (error?.authRequired) failure.authRequired = true;
64
+ const failures = Array.isArray(error?.failures) ? error.failures : [failure];
54
65
  state.mcpFailures = [...(state.mcpFailures || []), ...failures];
55
66
  }
56
67
  }
@@ -72,7 +83,13 @@ export function createMcpConnect({
72
83
  ]);
73
84
  }
74
85
 
75
- async function connectConfiguredMcp({ reset = false, only = null, enabled = true, reconcile = false } = {}) {
86
+ async function connectConfiguredMcp({
87
+ reset = false,
88
+ only = null,
89
+ enabled = true,
90
+ reconcile = false,
91
+ login = null,
92
+ } = {}) {
76
93
  if (envFlag('MIXDOG_DISABLE_MCP')) {
77
94
  ++state.mcpConnectGeneration;
78
95
  state.mcpFailures = [];
@@ -87,6 +104,9 @@ export function createMcpConnect({
87
104
  // been requested in the meantime. Registering as in-flight makes a later
88
105
  // reset serialize behind us instead of interleaving disconnect/connect.
89
106
  if (only || reconcile) {
107
+ // A disable/remove must not wait out an interactive login of the
108
+ // connect it queues behind: cancel that login first.
109
+ if (only && enabled === false) cancelMcpLogin(only);
90
110
  // Atomically capture the current generation AND the prior in-flight
91
111
  // promise in the same synchronous step, then chain our op onto it. No
92
112
  // await sits between the capture and the `state.mcpConnectInFlight = run`
@@ -106,7 +126,7 @@ export function createMcpConnect({
106
126
  }
107
127
  if (gen !== state.mcpConnectGeneration) return;
108
128
  if (reconcile) await reconcileServers();
109
- else await applyMcpServerConnection(only, enabled);
129
+ else await applyMcpServerConnection(only, enabled, undefined, login);
110
130
  })();
111
131
  state.mcpConnectInFlight = run;
112
132
  try {
@@ -207,5 +227,5 @@ export function createMcpConnect({
207
227
  }
208
228
  }
209
229
 
210
- return { connectConfiguredMcp, awaitInitialMcpConnect };
230
+ return { connectConfiguredMcp, awaitInitialMcpConnect, cancelMcpLogin, completeMcpLogin };
211
231
  }
@@ -5,9 +5,11 @@ import { clean } from '../../runtime/agent/orchestrator/runtime-core/session-tex
5
5
  import { envFlag } from '../../runtime/shared/env.mjs';
6
6
  import { resolveMcpServerCatalog } from './plugin-servers.mjs';
7
7
 
8
- function mcpServerStatus(cfg, live, fail) {
8
+ function mcpServerStatus(cfg, live, fail, loginPending) {
9
9
  if (cfg?.enabled === false) return 'disabled';
10
10
  if (live) return 'connected';
11
+ // Until an interactive sign-in completes the server still needs it.
12
+ if (loginPending || fail?.authRequired) return 'sign-in required';
11
13
  return fail ? 'failed' : 'disconnected';
12
14
  }
13
15
 
@@ -43,6 +45,12 @@ export function createMcpServerCatalog({
43
45
  * runtime's session cwd. */
44
46
  function resolveCatalog() {
45
47
  if (envFlag('MIXDOG_DISABLE_MCP')) return EMPTY_CATALOG;
48
+ return resolveConfiguredCatalog();
49
+ }
50
+
51
+ /** Every configured server (user config plus plugin declarations, disabled
52
+ * ones included) whether or not this runtime connects MCP at all. */
53
+ function resolveConfiguredCatalog() {
46
54
  let plugins = [];
47
55
  let registryWarning = null;
48
56
  try {
@@ -115,20 +123,28 @@ export function createMcpServerCatalog({
115
123
  const fail =
116
124
  failures.get(name) || (dead ? { msg: live.error || live.lastError || 'MCP server is not alive' } : undefined);
117
125
  const entry = entries[name] || {};
126
+ const loginPending = !(live && !dead) && mcpClient.isMcpLoginPending?.(name, getMcpScopeId()) === true;
127
+ const status = mcpServerStatus(cfg, live && !dead, fail, loginPending);
128
+ // Needing an OAuth sign-in is a state, not an error.
129
+ const authRequired = status === 'sign-in required';
118
130
  const row = {
119
131
  name,
120
132
  configured: true,
121
133
  enabled: cfg?.enabled !== false,
122
134
  connected: Boolean(live) && !dead,
123
- status: mcpServerStatus(cfg, live && !dead, fail),
135
+ status,
124
136
  transport: mcpTransportLabel(cfg),
125
137
  toolCount: live?.toolCount || 0,
126
138
  tools: live?.tools || [],
127
139
  capabilities: live?.capabilities || { tools: false, prompts: false, resources: false },
128
- error: fail?.msg || null,
140
+ error: authRequired ? null : fail?.msg || null,
129
141
  warnings: [...(entry.warnings || []), ...(live?.warnings || [])],
130
142
  source: sources[name] || 'config',
131
143
  };
144
+ if (authRequired) row.authRequired = true;
145
+ const oauth = mcpClient.getMcpServerOAuthState?.(cfg);
146
+ // Why the last sign-in did not complete (already credential-scrubbed).
147
+ if (oauth) row.oauth = authRequired && fail?.msg ? { ...oauth, lastError: fail.msg } : oauth;
132
148
  if (entry.owner) {
133
149
  row.owner = entry.owner;
134
150
  row.pluginEnabled = entry.pluginEnabled;
@@ -180,6 +196,7 @@ export function createMcpServerCatalog({
180
196
  return {
181
197
  mcpTransportLabel,
182
198
  resolveCatalog,
199
+ resolveConfiguredCatalog,
183
200
  resolveEffectiveMcpServers,
184
201
  pluginServer,
185
202
  getMcpServerConfig,
@@ -37,11 +37,14 @@ export function createMcpGlue({
37
37
  return {
38
38
  mcpStatus: catalog.mcpStatus,
39
39
  resolveMcpCatalog: catalog.resolveCatalog,
40
+ resolveConfiguredMcpCatalog: catalog.resolveConfiguredCatalog,
40
41
  pluginMcpServer: catalog.pluginServer,
41
42
  pluginMcpSummary: catalog.pluginMcpSummary,
42
43
  getMcpServerConfig: catalog.getMcpServerConfig,
43
44
  connectConfiguredMcp: connect.connectConfiguredMcp,
44
45
  awaitInitialMcpConnect: connect.awaitInitialMcpConnect,
46
+ cancelMcpLogin: connect.cancelMcpLogin,
47
+ completeMcpLogin: connect.completeMcpLogin,
45
48
  normalizeMcpServerInput,
46
49
  };
47
50
  }