mixdog 0.9.95 → 0.9.96

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 (178) hide show
  1. package/LICENSES/Apache-2.0.txt +201 -0
  2. package/LICENSES/MIT.txt +56 -0
  3. package/LICENSES/codex-NOTICE.txt +6 -0
  4. package/NOTICE.md +46 -9
  5. package/package.json +12 -9
  6. package/scripts/.tmp-coverage-report.mjs +96 -0
  7. package/scripts/build-tui.mjs +17 -1
  8. package/scripts/fixtures/patch-replay-corpus.json +98 -0
  9. package/src/defaults/mixdog-config.template.json +5 -8
  10. package/src/rules/agent/30-explorer.md +7 -4
  11. package/src/rules/shared/01-tool.md +20 -29
  12. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
  13. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
  14. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
  15. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
  16. package/src/runtime/agent/orchestrator/config.mjs +246 -67
  17. package/src/runtime/agent/orchestrator/mcp/client.mjs +26 -11
  18. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
  19. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
  20. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
  21. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
  22. package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
  23. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
  24. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
  25. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
  26. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
  27. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
  28. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
  29. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
  30. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
  31. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
  32. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
  33. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +28 -4
  34. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -33
  35. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
  36. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
  37. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
  38. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -27
  39. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -10
  40. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +5 -0
  41. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
  42. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
  43. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
  44. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
  45. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
  46. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  47. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
  48. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +12 -0
  49. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -33
  50. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
  51. package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
  52. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  53. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
  54. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +118 -18
  55. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
  56. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
  57. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
  58. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
  59. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +10 -26
  60. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +1 -1
  61. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +3 -3
  62. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -23
  63. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +23 -9
  64. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
  65. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
  66. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
  67. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
  70. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
  71. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
  72. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +87 -15
  73. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +6 -15
  74. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +2 -2
  75. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
  76. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
  77. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +29 -1
  78. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
  79. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
  80. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +10 -10
  81. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +56 -8
  82. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  83. package/src/runtime/channels/lib/config.mjs +2 -2
  84. package/src/runtime/channels/lib/inbound-handler.mjs +1 -1
  85. package/src/runtime/channels/lib/scheduler.mjs +2 -2
  86. package/src/runtime/memory/index.mjs +5 -38
  87. package/src/runtime/memory/lib/agent-ipc.mjs +145 -81
  88. package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
  89. package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
  90. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +12 -33
  91. package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
  92. package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
  93. package/src/runtime/memory/lib/http-router.mjs +8 -0
  94. package/src/runtime/memory/lib/ko-morph.mjs +3 -20
  95. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
  96. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
  97. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
  98. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
  99. package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
  100. package/src/runtime/memory/lib/pg/process.mjs +32 -9
  101. package/src/runtime/memory/tool-defs.mjs +1 -5
  102. package/src/runtime/search/tool-defs.mjs +2 -2
  103. package/src/runtime/shared/agent-route-config.mjs +119 -0
  104. package/src/runtime/shared/atomic-file.mjs +80 -4
  105. package/src/runtime/shared/channel-notification-routing.mjs +2 -2
  106. package/src/runtime/shared/child-guardian.mjs +204 -108
  107. package/src/runtime/shared/child-spawn-gate.mjs +60 -31
  108. package/src/runtime/shared/config.mjs +130 -44
  109. package/src/runtime/shared/resource-admission.mjs +146 -97
  110. package/src/runtime/shared/stream-progress.mjs +6 -0
  111. package/src/runtime/shared/tool-surface.mjs +0 -5
  112. package/src/runtime/shared/turn-snapshot.mjs +31 -5
  113. package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
  114. package/src/session-runtime/config-helpers.mjs +39 -37
  115. package/src/session-runtime/model-route-api.mjs +7 -5
  116. package/src/session-runtime/provider-auth-api.mjs +0 -7
  117. package/src/session-runtime/runtime-core.mjs +11 -11
  118. package/src/session-runtime/runtime-tunables.mjs +4 -4
  119. package/src/session-runtime/session-lifecycle.mjs +9 -2
  120. package/src/session-runtime/session-turn-api.mjs +41 -9
  121. package/src/session-runtime/settings-api.mjs +5 -17
  122. package/src/session-runtime/tool-defs.mjs +3 -3
  123. package/src/session-runtime/workflow-agents-api.mjs +55 -81
  124. package/src/session-runtime/workflow.mjs +14 -24
  125. package/src/standalone/agent-dispatch-broker.mjs +198 -0
  126. package/src/standalone/agent-tool/helpers.mjs +2 -2
  127. package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
  128. package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
  129. package/src/standalone/agent-tool/tool-def.mjs +0 -14
  130. package/src/standalone/backend-daemon.mjs +97 -36
  131. package/src/standalone/channel-admin.mjs +12 -12
  132. package/src/standalone/channel-daemon-client.mjs +17 -1
  133. package/src/standalone/channel-daemon-transport.mjs +90 -8
  134. package/src/standalone/engine-daemon-client.mjs +380 -205
  135. package/src/standalone/engine-daemon-protocol.mjs +3 -4
  136. package/src/standalone/engine-daemon-service.mjs +378 -134
  137. package/src/standalone/engine-daemon-transport.mjs +268 -77
  138. package/src/standalone/explore-tool.mjs +187 -70
  139. package/src/standalone/fair-call-scheduler.mjs +264 -0
  140. package/src/standalone/session-protocol.mjs +188 -0
  141. package/src/tui/App.jsx +30 -17
  142. package/src/tui/app/app-view.jsx +5 -2
  143. package/src/tui/app/core-memory-picker.mjs +19 -19
  144. package/src/tui/app/doctor.mjs +5 -11
  145. package/src/tui/app/message-selector.mjs +103 -0
  146. package/src/tui/app/onboarding-steps.mjs +1 -2
  147. package/src/tui/app/project-picker.mjs +80 -56
  148. package/src/tui/app/prompt-submit.mjs +46 -41
  149. package/src/tui/app/resume-picker.mjs +2 -2
  150. package/src/tui/app/route-pickers.mjs +3 -1
  151. package/src/tui/app/settings-picker.mjs +6 -8
  152. package/src/tui/app/transcript-window.mjs +38 -0
  153. package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
  154. package/src/tui/app/use-prompt-handlers.mjs +86 -21
  155. package/src/tui/app/use-prompt-hint.mjs +3 -2
  156. package/src/tui/app/use-prompt-queue-history.mjs +10 -3
  157. package/src/tui/app/use-transcript-scroll.mjs +9 -2
  158. package/src/tui/app/use-transcript-window.mjs +50 -17
  159. package/src/tui/components/PromptInput.jsx +71 -38
  160. package/src/tui/components/Spinner.jsx +1 -1
  161. package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
  162. package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
  163. package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
  164. package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
  165. package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
  166. package/src/tui/dist/index.mjs +920 -1592
  167. package/src/tui/engine/live-share.mjs +34 -0
  168. package/src/tui/engine/session-api-ext.mjs +1 -4
  169. package/src/tui/engine/session-api.mjs +103 -33
  170. package/src/tui/engine/session-flow.mjs +7 -1
  171. package/src/tui/engine/tool-card-results.mjs +4 -2
  172. package/src/tui/engine/tool-result-text.mjs +10 -0
  173. package/src/tui/engine/turn.mjs +9 -69
  174. package/src/tui/engine-local-session.mjs +47 -31
  175. package/src/tui/index.jsx +1 -2
  176. package/src/tui/spinner-meta.mjs +4 -4
  177. package/src/tui/spinner-verbs.mjs +51 -3
  178. package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
@@ -83,6 +83,8 @@ function createDiffTracker(meta = {}) {
83
83
  return {
84
84
  ownerSessionId: clean(meta.ownerSessionId) || null,
85
85
  ownerGeneration: Number(meta.ownerGeneration) || 0,
86
+ worktreeRequest: clean(meta.worktreeRequest),
87
+ worktreeContended: false,
86
88
  valid: true,
87
89
  sealed: false,
88
90
  revision: 0,
@@ -336,13 +338,31 @@ export async function beginTurnSnapshot(_worktree, sessionId) {
336
338
  generation,
337
339
  agents: new Map(),
338
340
  });
339
- const tracker = resetDiffTracker(ownerSessionId);
341
+ const worktreeRequest = pathKey(_worktree);
342
+ const tracker = resetDiffTracker(ownerSessionId, { worktreeRequest });
340
343
  trimTurnCache();
341
344
  if (!tracker) return;
345
+ for (const [otherSessionId, other] of _diffTrackersBySession) {
346
+ if (otherSessionId === ownerSessionId || other.sealed) continue;
347
+ if (!worktreeRequest || other.worktreeRequest !== worktreeRequest) continue;
348
+ tracker.worktreeContended = true;
349
+ other.worktreeContended = true;
350
+ }
342
351
  try {
343
352
  const snapshot = await createTurnWorktreeSnapshot(_worktree);
344
353
  if (_diffTrackersBySession.get(ownerSessionId) === tracker) {
345
354
  tracker.worktreeSnapshot = snapshot;
355
+ if (snapshot?.root) {
356
+ for (const [otherSessionId, other] of _diffTrackersBySession) {
357
+ if (otherSessionId === ownerSessionId) continue;
358
+ if (other.worktreeSnapshot?.root !== snapshot.root) continue;
359
+ // A whole-worktree baseline cannot attribute concurrent mutations
360
+ // to either session. Mark both turns permanently contended and fall
361
+ // back to their exact session-owned mutation trackers.
362
+ tracker.worktreeContended = true;
363
+ other.worktreeContended = true;
364
+ }
365
+ }
346
366
  }
347
367
  } catch {
348
368
  // Git is optional. Exact apply_patch tracking remains the fallback.
@@ -421,7 +441,7 @@ export function completeAgentTurnReview(handle, patches = []) {
421
441
  export async function completeTurnSnapshot(sessionId) {
422
442
  const tracker = _diffTrackersBySession.get(clean(sessionId));
423
443
  if (!tracker) return false;
424
- if (tracker.worktreeSnapshot) {
444
+ if (tracker.worktreeSnapshot && !tracker.worktreeContended) {
425
445
  try { await refreshTurnWorktreeSnapshot(tracker.worktreeSnapshot); } catch {}
426
446
  }
427
447
  tracker.sealed = true;
@@ -435,10 +455,13 @@ export async function getTurnReviewDiff(_worktree, sessionId) {
435
455
  const ownerSessionId = clean(sessionId);
436
456
  const turn = _turnsBySession.get(ownerSessionId);
437
457
  const tracker = _diffTrackersBySession.get(ownerSessionId);
438
- if (tracker?.worktreeSnapshot && !tracker.sealed) {
439
- try { await refreshTurnWorktreeSnapshot(tracker.worktreeSnapshot); } catch {}
458
+ const isolatedWorktreeSnapshot = tracker?.worktreeContended
459
+ ? null
460
+ : tracker?.worktreeSnapshot || null;
461
+ if (isolatedWorktreeSnapshot && !tracker.sealed) {
462
+ try { await refreshTurnWorktreeSnapshot(isolatedWorktreeSnapshot); } catch {}
440
463
  }
441
- const snapshot = tracker?.worktreeSnapshot || null;
464
+ const snapshot = isolatedWorktreeSnapshot;
442
465
  return {
443
466
  supported: true,
444
467
  files: snapshot?.files || [],
@@ -455,6 +478,9 @@ export async function getTurnReviewDiff(_worktree, sessionId) {
455
478
  export async function revertTurnReviewFile(_worktree, sessionId, file) {
456
479
  const ownerSessionId = clean(sessionId);
457
480
  const tracker = _diffTrackersBySession.get(ownerSessionId);
481
+ if (tracker?.worktreeContended) {
482
+ throw new Error('turn review revert is unavailable while sessions share a worktree');
483
+ }
458
484
  if (!tracker?.worktreeSnapshot) throw new Error('turn worktree snapshot is unavailable');
459
485
  await revertTurnWorktreeFile(tracker.worktreeSnapshot, file);
460
486
  return await getTurnReviewDiff(_worktree, ownerSessionId);
@@ -43,15 +43,17 @@ function runGit(args, {
43
43
  allowFailure = false,
44
44
  } = {}) {
45
45
  return new Promise((resolvePromise, rejectPromise) => {
46
+ const hasInput = (typeof input === 'string' || Buffer.isBuffer(input)) && input.length > 0;
46
47
  const child = spawn('git', args, {
47
48
  cwd,
48
49
  windowsHide: true,
49
- stdio: ['pipe', 'pipe', 'pipe'],
50
+ stdio: [hasInput ? 'pipe' : 'ignore', 'pipe', 'pipe'],
50
51
  });
51
52
  const stdout = [];
52
53
  const stderr = [];
53
54
  let stdoutBytes = 0;
54
55
  let stderrBytes = 0;
56
+ let stdinError = null;
55
57
  let settled = false;
56
58
  const finish = (error, result) => {
57
59
  if (settled) return;
@@ -93,9 +95,19 @@ function runGit(args, {
93
95
  finish(commandError(args, result.stderr, result.code));
94
96
  return;
95
97
  }
98
+ if (stdinError && result.code === 0) {
99
+ finish(stdinError);
100
+ return;
101
+ }
96
102
  finish(null, result);
97
103
  });
98
- child.stdin.end(input);
104
+ if (child.stdin) {
105
+ // A short-lived git command can close before Node flushes stdin. Keep
106
+ // that transport race inside this promise instead of emitting an
107
+ // unhandled EPIPE that terminates the entire release validator.
108
+ child.stdin.on('error', (error) => { stdinError = error; });
109
+ child.stdin.end(input);
110
+ }
99
111
  });
100
112
  }
101
113
 
@@ -7,11 +7,13 @@ import { routeFastKey, fastPreferenceFor } from './model-capabilities.mjs';
7
7
  export const DEFAULT_PROVIDER = 'anthropic-oauth';
8
8
  export const DEFAULT_MODEL = '';
9
9
 
10
- // Resolve the provider to use when a route carries no explicit provider.
11
- // Priority: config.defaultProvider (when it names a known provider) > DEFAULT_PROVIDER.
10
+ // Resolve a provider-less MAIN selector against the selected Main preset.
11
+ // DEFAULT_PROVIDER is only the unconfigured bootstrap fallback (model remains
12
+ // empty); agent/search override resolution never calls this to synthesize one.
12
13
  export function makeResolveDefaultProvider(isKnownProvider) {
13
14
  return function resolveDefaultProvider(config) {
14
- const configured = clean(config?.defaultProvider);
15
+ const selectedMain = findPreset(config, config?.default);
16
+ const configured = clean(selectedMain?.provider);
15
17
  if (configured && isKnownProvider(configured)) return configured;
16
18
  return DEFAULT_PROVIDER;
17
19
  };
@@ -46,16 +48,18 @@ export function makeResolveRoute(resolveDefaultProvider) {
46
48
  if (explicitModel && !explicitProvider) {
47
49
  const preset = findPreset(config, explicitModel);
48
50
  if (preset) {
49
- const p = clean(preset.provider) || DEFAULT_PROVIDER;
51
+ const p = clean(preset.provider);
50
52
  const m = clean(preset.model) || DEFAULT_MODEL;
51
- const saved = modelSettingsFor(config, p, m);
52
- return {
53
- provider: p,
54
- model: m,
55
- preset,
56
- effort: hasExplicitEffort ? explicitEffort : normalizeSavedEffort(saved.effort ?? preset.effort),
57
- fast: hasExplicitFast ? explicitFast : (hasOwn(saved, 'fast') ? saved.fast === true : (preset.fast === true || fastPreferenceFor(config, p, m))),
58
- };
53
+ if (p && m) {
54
+ const saved = modelSettingsFor(config, p, m);
55
+ return {
56
+ provider: p,
57
+ model: m,
58
+ preset,
59
+ effort: hasExplicitEffort ? explicitEffort : normalizeSavedEffort(saved.effort ?? preset.effort),
60
+ fast: hasExplicitFast ? explicitFast : (hasOwn(saved, 'fast') ? saved.fast === true : (preset.fast === true || fastPreferenceFor(config, p, m))),
61
+ };
62
+ }
59
63
  }
60
64
  }
61
65
 
@@ -63,16 +67,18 @@ export function makeResolveRoute(resolveDefaultProvider) {
63
67
  const defaultKey = config?.default;
64
68
  const preset = findPreset(config, defaultKey);
65
69
  if (preset) {
66
- const p = clean(preset.provider) || DEFAULT_PROVIDER;
70
+ const p = clean(preset.provider);
67
71
  const m = clean(preset.model) || DEFAULT_MODEL;
68
- const saved = modelSettingsFor(config, p, m);
69
- return {
70
- provider: p,
71
- model: m,
72
- preset,
73
- effort: hasExplicitEffort ? explicitEffort : normalizeSavedEffort(saved.effort ?? preset.effort),
74
- fast: hasExplicitFast ? explicitFast : (hasOwn(saved, 'fast') ? saved.fast === true : (preset.fast === true || fastPreferenceFor(config, p, m))),
75
- };
72
+ if (p && m) {
73
+ const saved = modelSettingsFor(config, p, m);
74
+ return {
75
+ provider: p,
76
+ model: m,
77
+ preset,
78
+ effort: hasExplicitEffort ? explicitEffort : normalizeSavedEffort(saved.effort ?? preset.effort),
79
+ fast: hasExplicitFast ? explicitFast : (hasOwn(saved, 'fast') ? saved.fast === true : (preset.fast === true || fastPreferenceFor(config, p, m))),
80
+ };
81
+ }
76
82
  }
77
83
  }
78
84
 
@@ -197,14 +203,14 @@ export function resolveAgentTerminalReapMs(config, provider) {
197
203
  // fall back to the provider's default; else the global 1h default.
198
204
  export function resolveAutoClearIdleMs(config, provider) {
199
205
  const raw = config?.autoClear && typeof config.autoClear === 'object' ? config.autoClear : {};
200
- const idleMs = Number(raw.idleMs ?? raw.thresholdMs ?? raw.idleMillis);
206
+ const idleMs = Number(raw.idleMs);
201
207
  if (Number.isFinite(idleMs) && idleMs > 0) return Math.max(60_000, Math.round(idleMs));
202
- return autoClearIdleMsForProvider(provider, raw.providerIdleMs ?? raw.providerDefaults ?? raw.providers);
208
+ return autoClearIdleMsForProvider(provider, raw.providerIdleMs);
203
209
  }
204
210
 
205
211
  export function normalizeSystemShellConfig(value = {}) {
206
212
  const raw = value && typeof value === 'object' ? value : {};
207
- const command = clean(raw.command ?? raw.path ?? raw.executable ?? raw.shell);
213
+ const command = clean(raw.command);
208
214
  const envCommand = clean(process.env.MIXDOG_SHELL);
209
215
  return {
210
216
  command,
@@ -227,7 +233,7 @@ export function normalizeSystemShellCommand(value) {
227
233
 
228
234
  export function normalizeAutoClearConfig(value = {}) {
229
235
  const raw = value && typeof value === 'object' ? value : {};
230
- const idleMs = Number(raw.idleMs ?? raw.thresholdMs ?? raw.idleMillis);
236
+ const idleMs = Number(raw.idleMs);
231
237
  const hasExplicitIdle = Number.isFinite(idleMs) && idleMs > 0;
232
238
  const rawMinPct = Number(raw.minContextPercent);
233
239
  const minContextPercent = Number.isFinite(rawMinPct)
@@ -241,7 +247,7 @@ export function normalizeAutoClearConfig(value = {}) {
241
247
  enabled: raw.enabled !== false,
242
248
  idleMs: hasExplicitIdle ? Math.max(60_000, Math.round(idleMs)) : null,
243
249
  custom: hasExplicitIdle,
244
- providerIdleMs: normalizeAutoClearProviderIdleMs(raw.providerIdleMs ?? raw.providerDefaults ?? raw.providers),
250
+ providerIdleMs: normalizeAutoClearProviderIdleMs(raw.providerIdleMs),
245
251
  minContextPercent,
246
252
  };
247
253
  }
@@ -254,16 +260,15 @@ export function normalizeCompactTypeSetting(value, fallback = 'recall-fasttrack'
254
260
  return fallback;
255
261
  }
256
262
 
257
- export function normalizeCompactionConfig(value = {}, { memoryEnabled = true } = {}) {
263
+ export function normalizeCompactionConfig(value = {}) {
258
264
  const raw = value && typeof value === 'object' ? value : {};
259
- let compactType = normalizeCompactTypeSetting(raw.compactType ?? raw.compact_type ?? raw.type, 'recall-fasttrack');
260
- // Memory is now always-on, so recall-fasttrack no longer downgrades to
261
- // semantic. `memoryEnabled` is retained as a param for API compatibility but
262
- // is intentionally ignored (fasttrack drains run on-demand regardless of the
263
- // recap/background-cycle toggle).
264
- void memoryEnabled;
265
+ const compactType = normalizeCompactTypeSetting(raw.type, 'recall-fasttrack');
266
+ const next = { ...raw };
267
+ delete next.compactType;
268
+ delete next.compact_type;
269
+ delete next.enabled;
265
270
  return {
266
- ...raw,
271
+ ...next,
267
272
  auto: raw.auto !== false && raw.enabled !== false,
268
273
  type: compactType,
269
274
  compactType,
@@ -284,9 +289,6 @@ export function moduleEnabled(configLike, name, fallback = true) {
284
289
  export function recapEnabled(configLike, fallback = true) {
285
290
  const entry = configLike?.recap;
286
291
  if (entry && typeof entry === 'object' && entry.enabled === false) return false;
287
- // Legacy fallback: pre-migration configs may still carry modules.memory=false.
288
- if (configLike?.modules?.memory === false) return false;
289
- if (moduleEnabled(configLike, 'memory', true) === false) return false;
290
292
  return fallback !== false;
291
293
  }
292
294
 
@@ -64,11 +64,13 @@ export function createModelRouteApi(deps) {
64
64
  return await collectSearchProviderModels({ force: options.force === true || options.refresh === true });
65
65
  },
66
66
  async setSearchRoute(next) {
67
- const searchRoute = getSearchRouteState();
68
- let selectedRoute = normalizeSearchRouteConfig(next);
69
- if (!selectedRoute && next?.model && searchRoute?.provider) {
70
- selectedRoute = normalizeSearchRouteConfig({ ...next, provider: searchRoute.provider });
71
- }
67
+ let selectedRoute = clean(next?.provider)
68
+ ? normalizeSearchRouteConfig(next)
69
+ : normalizeSearchRouteConfig({
70
+ provider: SEARCH_DEFAULT_PROVIDER,
71
+ model: SEARCH_DEFAULT_MODEL,
72
+ ...(next?.toolType ? { toolType: next.toolType } : {}),
73
+ });
72
74
  if (!selectedRoute) throw new Error('search route requires provider and model');
73
75
  if (isDefaultSearchRouteConfig(selectedRoute)) {
74
76
  await awaitKeychainPrewarm();
@@ -193,12 +193,5 @@ export function createProviderAuthApi({
193
193
  quick: options.quick === true,
194
194
  });
195
195
  },
196
- async setDefaultProvider(provider) {
197
- const requested = clean(provider);
198
- if (!requested) throw new Error('provider is required');
199
- if (!isKnownProvider(requested)) throw new Error(`unknown provider "${provider}"`);
200
- saveConfigAndAdopt({ ...getConfig(), defaultProvider: requested });
201
- return requested;
202
- },
203
196
  };
204
197
  }
@@ -428,11 +428,8 @@ export async function createMixdogSessionRuntime({
428
428
  rt.searchModPromise = null;
429
429
  rt.codeGraphModPromise = null;
430
430
 
431
- // Memory module is always-on. `memoryEnabled()` is kept as a thin alias that
432
- // now always returns true (callers/compaction helpers still reference it);
433
- // the user-facing toggle is `recap` (background cycles only), read via
434
- // recapEnabled(config).
435
- const memoryEnabled = () => true;
431
+ // Memory is always-on. The user-facing toggle is `recap` (background cycles
432
+ // only), read via recapEnabled(config).
436
433
  const recapEnabledFn = () => recapEnabled(rt.config, true);
437
434
  const channelsEnabled = () => moduleEnabled(rt.config, 'channels', true);
438
435
 
@@ -469,10 +466,6 @@ export async function createMixdogSessionRuntime({
469
466
 
470
467
  const nextConfig = { ...(rt.config || {}) };
471
468
  nextConfig.presets = upsertWorkflowPreset(nextConfig.presets, 'lead', leadRoute);
472
- nextConfig.workflowRoutes = {
473
- ...(nextConfig.workflowRoutes || {}),
474
- lead: leadRoute,
475
- };
476
469
  nextConfig.default = workflowPresetId('lead');
477
470
 
478
471
  saveConfigAndAdopt(nextConfig);
@@ -1198,7 +1191,6 @@ export async function createMixdogSessionRuntime({
1198
1191
  reg,
1199
1192
  cfgMod,
1200
1193
  activeWorkflowContext,
1201
- memoryEnabled,
1202
1194
  hooks,
1203
1195
  hookCommonPayload,
1204
1196
  mcpClient,
@@ -1368,7 +1360,6 @@ export async function createMixdogSessionRuntime({
1368
1360
  parseDurationMs,
1369
1361
  formatDurationMs,
1370
1362
  localPackageVersion,
1371
- memoryEnabled,
1372
1363
  recapEnabledFn,
1373
1364
  channelsEnabled,
1374
1365
  autoUpdateEnabled,
@@ -1651,6 +1642,15 @@ export async function createMixdogSessionRuntime({
1651
1642
  throw new Error(`session ${rt.session.id} is already materialized`);
1652
1643
  }
1653
1644
  rt.reservedSessionId = id;
1645
+ // Reservation is the earliest safe point to prepare keychain, memory,
1646
+ // provider metadata, hooks, and the provider transport. This starts no
1647
+ // model response and therefore incurs no inference/token usage. The first
1648
+ // submit joins this single-flight promise instead of paying cold setup.
1649
+ void createCurrentSession('reservation').catch((error) => {
1650
+ bootProfile('session:reservation-prewarm-failed', {
1651
+ error: error?.message || String(error),
1652
+ });
1653
+ });
1654
1654
  return id;
1655
1655
  },
1656
1656
  get provider() {
@@ -24,10 +24,10 @@ export function readRuntimeTunables() {
24
24
  statuslineUsageRefreshDelayMs: envDelayMs('MIXDOG_STATUSLINE_USAGE_REFRESH_MS', 240_000, { min: 30_000, max: 540_000 }),
25
25
  channelStartDelayMs: envDelayMs('MIXDOG_CHANNEL_START_DELAY_MS', 10_000, { min: 0, max: 120_000 }),
26
26
  backgroundBusyRetryMs: envDelayMs('MIXDOG_BACKGROUND_BUSY_RETRY_MS', 1_000, { min: 50, max: 10_000 }),
27
- // MCP startup must not own the first-token critical path. Servers that do
28
- // not connect inside this small grace period join through the existing
29
- // late-tool announcement path on the next turn.
30
- mcpTurnGraceMs: envDelayMs('MIXDOG_MCP_TURN_GRACE_MS', 150, { min: 0, max: 2_000 }),
27
+ // MCP startup never owns the first-token critical path by default. Already
28
+ // connected tools fold synchronously; in-flight servers join through the
29
+ // late-tool announcement path. Operators may opt back into a grace window.
30
+ mcpTurnGraceMs: envDelayMs('MIXDOG_MCP_TURN_GRACE_MS', 0, { min: 0, max: 2_000 }),
31
31
  // Boot-time remote start is deferred past the first frame; see the caller.
32
32
  remoteAutoStartDelayMs: envDelayMs('MIXDOG_REMOTE_AUTOSTART_DELAY_MS', 1_500, { min: 0, max: 60_000 }),
33
33
  providerWarmupEnabled: !envFlag('MIXDOG_DISABLE_PROVIDER_WARMUP')
@@ -30,7 +30,6 @@ export function createSessionLifecycle({
30
30
  reg,
31
31
  cfgMod,
32
32
  activeWorkflowContext,
33
- memoryEnabled,
34
33
  hooks,
35
34
  hookCommonPayload,
36
35
  mcpClient,
@@ -140,6 +139,14 @@ export function createSessionLifecycle({
140
139
 
141
140
  const startedAt = performance.now();
142
141
  bootProfile('session:create:start', { mode: rt.mode, reason });
142
+ // A daemon reservation is deliberate user-think-time prewarm. Start the
143
+ // one-time agent-loop module load now so the first real prompt does not pay
144
+ // its ~100ms dynamic-import graph immediately before provider.send.
145
+ if (reason === 'reservation' && typeof mgr.prewarmAgentLoop === 'function') {
146
+ void mgr.prewarmAgentLoop().catch((error) => {
147
+ bootProfile('agent-loop:prewarm-failed', { error: error?.message || String(error) });
148
+ });
149
+ }
143
150
  const promise = (async () => {
144
151
  // Demand-only: this starts only after the user submits (unless an
145
152
  // explicitly enabled prewarm caller asks for a session). Core-memory
@@ -187,7 +194,7 @@ export function createSessionLifecycle({
187
194
  workflowContext,
188
195
  fast: rt.route.fast === true,
189
196
  compaction: rt.config.compaction && typeof rt.config.compaction === 'object'
190
- ? normalizeCompactionConfig(rt.config.compaction, { memoryEnabled: memoryEnabled() })
197
+ ? normalizeCompactionConfig(rt.config.compaction)
191
198
  : undefined,
192
199
  };
193
200
  if (hasOwn(rt.route, 'effort') || rt.route.effectiveEffort) {
@@ -12,6 +12,7 @@ import {
12
12
  import { getMcpTools } from '../runtime/agent/orchestrator/mcp/client.mjs';
13
13
  import { traceTurnTiming } from '../runtime/agent/orchestrator/agent-trace.mjs';
14
14
  import { beginTurnSnapshot, completeTurnSnapshot } from '../runtime/shared/turn-snapshot.mjs';
15
+ import { isVisibleStreamProgress } from '../runtime/shared/stream-progress.mjs';
15
16
 
16
17
  export function splitToolStatusCounts(rows) {
17
18
  const list = Array.isArray(rows) ? rows : [];
@@ -44,7 +45,7 @@ export function createSessionTurnApi(deps) {
44
45
  scheduleProviderWarmup, scheduleProviderModelWarmup, invalidateContextStatusCache,
45
46
  agentTool, recreateCurrentSessionIfReady, invalidatePreSessionToolSurface,
46
47
  activeToolSurface, applyResolvedCwd, resolveCwdPath, agentStatusState, notificationListeners,
47
- awaitInitialMcpConnect, mcpTurnGraceMs = 150, awaitRoutePreparation,
48
+ awaitInitialMcpConnect, mcpTurnGraceMs = 0, awaitRoutePreparation,
48
49
  getReservedSessionId, sessionTitles,
49
50
  } = deps;
50
51
  const enqueueRemoteAttachedPrompt = (prompt) => {
@@ -79,7 +80,14 @@ export function createSessionTurnApi(deps) {
79
80
  // a normal user turn and this surface refreshes from disk.
80
81
  const attachedSession = getSession();
81
82
  if (attachedSession?.remoteAttached) {
82
- const delivered = enqueueRemoteAttachedPrompt(prompt);
83
+ // Carry the submission id into the spool: the owner reuses it for the
84
+ // queue entry and the settled user row, so the submitting surface's
85
+ // optimistic bubble is released instead of standing beside the
86
+ // mirrored twin (user: 같은 세션에서 메세지가 두 개로 보인다).
87
+ const submissionId = String(options.id || '').trim();
88
+ const delivered = enqueueRemoteAttachedPrompt(
89
+ submissionId ? { content: prompt, id: submissionId } : prompt,
90
+ );
83
91
  return {
84
92
  // This branch is only a race-safe fallback for callers that reached
85
93
  // ask() before the live pipe was ready. Never manufacture an
@@ -117,9 +125,10 @@ export function createSessionTurnApi(deps) {
117
125
  let turnTimingEmitted = false;
118
126
  // Heavy runtime warmup is one-shot and demand-driven: idle desktop panes
119
127
  // never spawn shell/token/shard helpers or a code-graph worker. More
120
- // importantly, do not start them until the provider has produced its
121
- // first stream delta: launching PowerShell + graph workers before that
122
- // point directly competes with the first-token critical path.
128
+ // importantly, do not start them until the provider has produced visible
129
+ // text/reasoning/tool progress: transport headers and response-created
130
+ // acknowledgements arrive earlier and would make PowerShell + graph
131
+ // workers compete with the actual first-token critical path.
123
132
  const heavyRuntimeWarmupPending = typeof getCodeGraphFirstTurnPrewarmDone === 'function'
124
133
  && !getCodeGraphFirstTurnPrewarmDone();
125
134
  if (heavyRuntimeWarmupPending) {
@@ -264,7 +273,6 @@ export function createSessionTurnApi(deps) {
264
273
  .map((part) => String(part || '').trim())
265
274
  .filter(Boolean)
266
275
  .join('\n\n');
267
- providerStartedAt = performance.now();
268
276
  const result = await mgr.askSession(
269
277
  session0.id,
270
278
  prompt,
@@ -322,14 +330,26 @@ export function createSessionTurnApi(deps) {
322
330
  onToolApproval: options.onToolApproval,
323
331
  onCompactEvent: options.onCompactEvent,
324
332
  onStageChange: options.onStageChange,
333
+ onProviderSendStarted: (...args) => {
334
+ // This callback is emitted immediately before provider.send,
335
+ // after session locking, history shaping and pre-send compact.
336
+ // The old timestamp above mgr.askSession mislabeled all of that
337
+ // Mixdog work as provider latency.
338
+ if (!providerStartedAt) providerStartedAt = performance.now();
339
+ return options.onProviderSendStarted?.(...args);
340
+ },
341
+ onToolPhaseStarted: options.onToolPhaseStarted,
342
+ onToolPhaseCompleted: options.onToolPhaseCompleted,
325
343
  onStreamDelta: (...args) => {
326
344
  let value;
327
345
  try {
328
346
  value = options.onStreamDelta?.(...args);
329
347
  } finally {
330
- turnTimingStatus = 'first-delta';
331
- emitTurnTiming(turnTimingStatus);
332
- armHeavyRuntimeWarmup('first-stream');
348
+ if (isVisibleStreamProgress(args[0])) {
349
+ turnTimingStatus = 'first-visible';
350
+ emitTurnTiming(turnTimingStatus);
351
+ armHeavyRuntimeWarmup('first-visible');
352
+ }
333
353
  }
334
354
  return value;
335
355
  },
@@ -399,6 +419,18 @@ export function createSessionTurnApi(deps) {
399
419
  pushTranscriptRebind?.();
400
420
  return true;
401
421
  },
422
+ // Message-selector rewind: drop the chosen user prompt and everything
423
+ // after it from the model history. Unlike clear() the session object is
424
+ // kept (same id, same provider/model/cwd) — only the tail is undone.
425
+ async rewindMessages(options = {}) {
426
+ const session = getSession();
427
+ if (!session?.id) return null;
428
+ const result = await mgr.rewindSessionMessagesTo?.(session.id, options);
429
+ if (!result) return null;
430
+ setSession(mgr.getSession(session.id) || session);
431
+ invalidateContextStatusCache();
432
+ return result;
433
+ },
402
434
  // session_manage tool handoff: the engine polls this at turn end and, if
403
435
  // set, runs the same clear path the idle auto-clear uses. One-shot read.
404
436
  consumePendingSessionReset() {
@@ -35,7 +35,6 @@ export function createSettingsApi({
35
35
  formatDurationMs,
36
36
  localPackageVersion,
37
37
  // state getters / feature flags
38
- memoryEnabled,
39
38
  recapEnabledFn,
40
39
  channelsEnabled,
41
40
  autoUpdateEnabled,
@@ -173,11 +172,11 @@ export function createSettingsApi({
173
172
  },
174
173
  getCompactionSettings() {
175
174
  const config = getConfig();
176
- return normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() });
175
+ return normalizeCompactionConfig(config.compaction);
177
176
  },
178
177
  setCompactionSettings(input = {}) {
179
178
  const config = getConfig();
180
- const current = normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() });
179
+ const current = normalizeCompactionConfig(config.compaction);
181
180
  const next = { ...current };
182
181
  if (hasOwn(input, 'auto')) next.auto = input.auto !== false;
183
182
  if (hasOwn(input, 'enabled')) next.auto = input.enabled !== false;
@@ -193,18 +192,18 @@ export function createSettingsApi({
193
192
  if (hasOwn(input, key)) next[key] = input[key];
194
193
  }
195
194
  const nextConfig = { ...config };
196
- nextConfig.compaction = normalizeCompactionConfig(next, { memoryEnabled: memoryEnabled() });
195
+ nextConfig.compaction = normalizeCompactionConfig(next);
197
196
  saveConfigAndAdopt(nextConfig);
198
197
  const config2 = getConfig();
199
198
  const session = getSession();
200
199
  if (session) {
201
200
  session.compaction = {
202
201
  ...(session.compaction || {}),
203
- ...normalizeCompactionConfig(config2.compaction, { memoryEnabled: memoryEnabled() }),
202
+ ...normalizeCompactionConfig(config2.compaction),
204
203
  };
205
204
  }
206
205
  invalidateContextStatusCache();
207
- return normalizeCompactionConfig(config2.compaction, { memoryEnabled: memoryEnabled() });
206
+ return normalizeCompactionConfig(config2.compaction);
208
207
  },
209
208
  // Recap toggle: user-facing switch that gates ONLY the background memory
210
209
  // cycles (1/2/3). The memory module (transcript watcher/ingest, on-demand
@@ -223,17 +222,6 @@ export function createSettingsApi({
223
222
  invalidateContextStatusCache();
224
223
  return this.getRecapSettings();
225
224
  },
226
- // Thin aliases kept for the current TUI callsites (updated separately).
227
- // enabled here reflects the recap toggle; memory itself is always-on.
228
- getMemorySettings() {
229
- return {
230
- enabled: recapEnabledFn(),
231
- compactFastTrackAvailable: true,
232
- };
233
- },
234
- async setMemoryEnabled(enabled) {
235
- return this.setRecapEnabled(enabled);
236
- },
237
225
  getChannelSettings(options = {}) {
238
226
  return {
239
227
  enabled: channelsEnabled(),
@@ -13,7 +13,7 @@ export const TOOL_SEARCH_TOOL = {
13
13
  openWorldHint: false,
14
14
  agentHidden: true,
15
15
  },
16
- description: 'Load deferred tools by exact name via names[]; reports loaded/already-active/missing plus MCP servers still connecting or failed. Deferred tools also auto-load when called directly.',
16
+ description: 'Deferred-tool activation status; direct calls auto-load.',
17
17
  inputSchema: {
18
18
  type: 'object',
19
19
  properties: {
@@ -34,7 +34,7 @@ export const CWD_TOOL = {
34
34
  openWorldHint: false,
35
35
  agentHidden: true,
36
36
  },
37
- description: 'Show or set the session work project for tool execution. Use only for explicit project path changes.',
37
+ description: 'Show or set the session work project for tool execution.',
38
38
  inputSchema: {
39
39
  type: 'object',
40
40
  properties: {
@@ -84,7 +84,7 @@ export const SESSION_MANAGE_TOOL = {
84
84
  openWorldHint: false,
85
85
  agentHidden: true,
86
86
  },
87
- description: 'Reset this conversation on explicit user request. action=clear wipes all context (like /clear); action=compact_clear summarizes first and carries context forward (auto-clear style). Applies when the current turn ends.',
87
+ description: 'Schedule conversation reset at current-turn end.',
88
88
  inputSchema: {
89
89
  type: 'object',
90
90
  properties: {