mixdog 0.9.52 → 0.9.53

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 (111) hide show
  1. package/package.json +1 -1
  2. package/scripts/bench-run.mjs +2 -2
  3. package/scripts/compact-pressure-test.mjs +104 -0
  4. package/scripts/desktop-session-bridge-test.mjs +704 -0
  5. package/scripts/freevar-smoke.mjs +7 -4
  6. package/scripts/lifecycle-api-test.mjs +65 -4
  7. package/scripts/max-output-recovery-test.mjs +31 -0
  8. package/scripts/memory-core-input-test.mjs +10 -0
  9. package/scripts/openai-oauth-ws-1006-retry-test.mjs +63 -3
  10. package/scripts/openai-ws-early-settle-test.mjs +40 -0
  11. package/scripts/parent-abort-link-test.mjs +24 -0
  12. package/scripts/process-lifecycle-test.mjs +80 -22
  13. package/scripts/provider-contract-test.mjs +257 -0
  14. package/scripts/provider-toolcall-test.mjs +172 -10
  15. package/scripts/session-orphan-sweep-test.mjs +27 -1
  16. package/src/lib/keychain-cjs.cjs +36 -23
  17. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -13
  18. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
  19. package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
  20. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
  21. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -300
  22. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -4
  23. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +18 -266
  24. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +18 -1
  25. package/src/runtime/agent/orchestrator/providers/gemini.mjs +15 -130
  26. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +5 -115
  27. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
  28. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
  29. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
  30. package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
  31. package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -71
  33. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +47 -3
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -7
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +72 -77
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -20
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
  39. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +37 -28
  40. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -7
  41. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -0
  42. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
  43. package/src/runtime/agent/orchestrator/session/context-utils.mjs +20 -223
  44. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
  45. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -5
  46. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
  47. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
  48. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +24 -0
  49. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
  50. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
  51. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -1
  52. package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
  53. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
  54. package/src/runtime/agent/orchestrator/session/store.mjs +305 -127
  55. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
  56. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
  57. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
  58. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
  59. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
  60. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
  61. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
  62. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +22 -298
  63. package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
  64. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -71
  65. package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
  66. package/src/runtime/channels/backends/discord.mjs +6 -6
  67. package/src/runtime/channels/lib/config.mjs +15 -2
  68. package/src/runtime/channels/lib/memory-client.mjs +20 -3
  69. package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
  70. package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
  71. package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
  72. package/src/runtime/channels/lib/worker-main.mjs +16 -5
  73. package/src/runtime/memory/index.mjs +24 -198
  74. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
  75. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
  76. package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
  77. package/src/runtime/memory/lib/pg/supervisor.mjs +0 -4
  78. package/src/runtime/memory/lib/query-handlers.mjs +2 -122
  79. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
  80. package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
  81. package/src/runtime/search/lib/http-fetch.mjs +1 -1
  82. package/src/runtime/shared/config.mjs +58 -13
  83. package/src/runtime/shared/llm/cost.mjs +14 -4
  84. package/src/runtime/shared/memory-snapshot.mjs +236 -0
  85. package/src/runtime/shared/process-lifecycle.mjs +92 -19
  86. package/src/runtime/shared/process-shutdown.mjs +6 -0
  87. package/src/runtime/shared/resource-admission.mjs +7 -2
  88. package/src/session-runtime/channel-config-api.mjs +7 -7
  89. package/src/session-runtime/config-lifecycle.mjs +20 -17
  90. package/src/session-runtime/cwd-plugins.mjs +9 -7
  91. package/src/session-runtime/env.mjs +1 -2
  92. package/src/session-runtime/hitch-profile.mjs +45 -0
  93. package/src/session-runtime/lifecycle-api.mjs +36 -9
  94. package/src/session-runtime/mcp-glue.mjs +6 -11
  95. package/src/session-runtime/provider-init-key.mjs +17 -0
  96. package/src/session-runtime/runtime-core.mjs +44 -103
  97. package/src/session-runtime/runtime-paths.mjs +20 -0
  98. package/src/session-runtime/runtime-tool-routing.mjs +55 -0
  99. package/src/session-runtime/tool-catalog-data.mjs +51 -0
  100. package/src/session-runtime/tool-catalog.mjs +15 -89
  101. package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
  102. package/src/standalone/agent-tool/worker-rows.mjs +93 -0
  103. package/src/standalone/agent-tool.mjs +12 -162
  104. package/src/standalone/channel-admin.mjs +29 -0
  105. package/src/tui/App.jsx +11 -5
  106. package/src/tui/dist/index.mjs +202 -65
  107. package/src/tui/engine/session-api-ext.mjs +37 -4
  108. package/src/tui/index.jsx +1 -0
  109. package/src/tui/lib/voice-setup.mjs +5 -5
  110. package/scripts/_devtools-stub.mjs +0 -1
  111. package/src/runtime/lib/keychain-cjs.cjs +0 -288
@@ -32,13 +32,15 @@ export function resolveResumeCwd(session, currentCwd) {
32
32
  // getters/setters for the mutable session/route/cwd locals plus the closure
33
33
  // callbacks and long-lived handles (managers, timers, channel/agent/mcp).
34
34
  export function createLifecycleApi(deps) {
35
+ const cancelBackgroundTasksForLifecycle = deps.cancelBackgroundTasks || cancelBackgroundTasks;
35
36
  const {
36
37
  getSession, setSession, getRoute, setRoute, getConfig, getMode, getCurrentCwd,
38
+ getDesktopSession, setDesktopSession,
37
39
  setCloseRequested, getMemoryModPromise, setMemoryModPromise,
38
40
  setSessionNeedsCwdRefresh,
39
41
  hooks, hookCommonPayload, mgr, statusRoutes, channels, agentTool, mcpClient,
40
42
  warmupTimers, prewarmTimers,
41
- flushConfigSave, flushBackendSave, flushOutputStyleSave,
43
+ flushAllConfigSavesAsync,
42
44
  withTeardownDeadline, closePatchRuntimeIfLoaded,
43
45
  createCurrentSession, refreshRouteEffort,
44
46
  invalidateContextStatusCache, invalidatePreSessionToolSurface,
@@ -76,12 +78,10 @@ export function createLifecycleApi(deps) {
76
78
  );
77
79
  }
78
80
  } catch { /* best-effort: SessionEnd hook must never wedge teardown */ }
79
- // Persist any change that is still sitting in the debounce window so a
80
- // toggle made right before exit is not lost. Synchronous + best-effort:
81
- // teardown must continue even if the final write fails.
82
- try { flushConfigSave(); } catch {}
83
- try { flushBackendSave(); } catch {}
84
- try { flushOutputStyleSave(); } catch {}
81
+ // Teardown stays async end-to-end across every writer sharing the config
82
+ // lock. Never start a synchronous lock wait while an in-process async
83
+ // holder still needs the event loop to finish and release it.
84
+ try { await flushAllConfigSavesAsync(); } catch {}
85
85
  try { hooks.flushRules?.(); } catch {}
86
86
  if (prewarmTimers.channelStartTimer) {
87
87
  clearTimeout(prewarmTimers.channelStartTimer);
@@ -226,6 +226,32 @@ export function createLifecycleApi(deps) {
226
226
  };
227
227
  }).filter(Boolean);
228
228
  },
229
+ async switchContext({ cwd, desktopSession: nextDesktopSession } = {}) {
230
+ const session = getSession();
231
+ if (session?.id) {
232
+ const cleanupReason = 'desktop-context-switch';
233
+ try {
234
+ cancelBackgroundTasksForLifecycle({
235
+ reason: cleanupReason,
236
+ notify: false,
237
+ callerSessionId: session.id,
238
+ });
239
+ } catch {}
240
+ try { agentTool?.closeAll?.(cleanupReason); } catch {}
241
+ statusRoutes?.clearGatewaySessionRoute?.(session.id);
242
+ const tombstone = !hasUserConversationMessage(session.messages)
243
+ && !hasUserConversationMessage(session.liveTurnMessages);
244
+ mgr.closeSession(session.id, cleanupReason, { tombstone });
245
+ setSession(null);
246
+ }
247
+ setDesktopSession(nextDesktopSession && typeof nextDesktopSession === 'object'
248
+ ? nextDesktopSession
249
+ : null);
250
+ await applyResolvedCwd(cwd, { markRefresh: false, waitForMcpReset: true });
251
+ invalidateContextStatusCache();
252
+ invalidatePreSessionToolSurface();
253
+ return true;
254
+ },
229
255
  async newSession() {
230
256
  const session = getSession();
231
257
  if (session?.id) {
@@ -247,8 +273,9 @@ export function createLifecycleApi(deps) {
247
273
  const previousId = prev?.id || null;
248
274
  const previousMessages = prev?.messages || null;
249
275
  const previousLive = prev?.liveTurnMessages || null;
250
- const resumeOptions = desktopSession && typeof desktopSession === 'object'
251
- ? { desktopSession }
276
+ const activeDesktopSession = getDesktopSession?.() ?? desktopSession;
277
+ const resumeOptions = activeDesktopSession && typeof activeDesktopSession === 'object'
278
+ ? { desktopSession: activeDesktopSession }
252
279
  : undefined;
253
280
  const resumed = await mgr.resumeSession(id, toolSpecForMode(getMode()), resumeOptions);
254
281
  if (!resumed) return null;
@@ -1,12 +1,10 @@
1
- // MCP config/status/connect glue, extracted from mixdog-session-runtime.mjs.
2
- // Dependency-injected factory: all live state (config, currentCwd, connect
3
- // generation/in-flight/failures) is threaded through accessors + a caller-owned
4
- // `state` object so the facade's teardown/reconnect paths still observe it.
5
- // Method behavior is byte-for-byte identical; only grouping changes.
1
+ // MCP config/status/connect glue. Mutable runtime state is dependency-injected
2
+ // through accessors and the caller-owned `state` object.
6
3
  import { resolve } from 'node:path';
7
4
  import { statSync } from 'node:fs';
8
5
  import { clean } from './session-text.mjs';
9
6
  import { normalizeMcpProjectPathKey, readProjectMcpServers } from './plugin-mcp.mjs';
7
+ import { envFlag } from './env.mjs';
10
8
 
11
9
  // Cache project-local `.mcp.json` reads by path + mtime so repeated mcpStatus()
12
10
  // calls skip existsSync+readFileSync+JSON.parse when the file is unchanged.
@@ -16,9 +14,6 @@ const PROJECT_MCP_CACHE_MAX = 32;
16
14
  export function invalidateProjectMcpCache(cwd) {
17
15
  projectMcpCache.delete(resolve(cwd || '.', '.mcp.json'));
18
16
  }
19
- function mcpDisabled() {
20
- return /^(?:1|true|on|yes)$/i.test(String(process.env.MIXDOG_DISABLE_MCP || ''));
21
- }
22
17
  function cachedProjectMcpServers(cwd) {
23
18
  const path = resolve(cwd || '.', '.mcp.json');
24
19
  let mtimeMs = 0;
@@ -55,7 +50,7 @@ export function createMcpGlue({
55
50
  // origin ('config' | 'project') for status reporting.
56
51
  function resolveEffectiveMcpServers() {
57
52
  // Benchmark/embedding guard: do not even inspect project `.mcp.json`.
58
- if (mcpDisabled()) return { servers: {}, sources: {} };
53
+ if (envFlag('MIXDOG_DISABLE_MCP')) return { servers: {}, sources: {} };
59
54
  const config = getConfig();
60
55
  const configured = config?.mcpServers && typeof config.mcpServers === 'object'
61
56
  ? config.mcpServers
@@ -78,7 +73,7 @@ export function createMcpGlue({
78
73
  }
79
74
 
80
75
  function mcpStatus() {
81
- if (mcpDisabled()) {
76
+ if (envFlag('MIXDOG_DISABLE_MCP')) {
82
77
  return { servers: [], configuredCount: 0, connectedCount: 0, failedCount: 0 };
83
78
  }
84
79
  const { servers: configured, sources } = resolveEffectiveMcpServers();
@@ -149,7 +144,7 @@ export function createMcpGlue({
149
144
  }
150
145
 
151
146
  async function connectConfiguredMcp({ reset = false, only = null, enabled = true } = {}) {
152
- if (mcpDisabled()) {
147
+ if (envFlag('MIXDOG_DISABLE_MCP')) {
153
148
  ++state.mcpConnectGeneration;
154
149
  state.mcpFailures = [];
155
150
  if (only) await mcpClient.disconnectMcpServer?.(only);
@@ -0,0 +1,17 @@
1
+ function sortKeysDeep(value) {
2
+ if (Array.isArray(value)) return value.map(sortKeysDeep);
3
+ if (value && typeof value === 'object') {
4
+ const out = {};
5
+ for (const key of Object.keys(value).sort()) out[key] = sortKeysDeep(value[key]);
6
+ return out;
7
+ }
8
+ return value;
9
+ }
10
+
11
+ export function providerInitCacheKey(value) {
12
+ try {
13
+ return JSON.stringify(sortKeysDeep(value));
14
+ } catch {
15
+ return `uncacheable:${Date.now()}:${Math.random()}`;
16
+ }
17
+ }
@@ -1,9 +1,8 @@
1
1
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
2
  import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
3
  import { performance } from 'node:perf_hooks';
6
4
  import httpMod from 'node:http';
5
+ import './hitch-profile.mjs';
7
6
  import { ensureStandaloneEnvironment } from '../standalone/seeds.mjs';
8
7
  import { createStandaloneAgent } from '../standalone/agent-tool.mjs';
9
8
  import { isAgentOwner } from '../runtime/agent/orchestrator/agent-owner.mjs';
@@ -19,7 +18,6 @@ import { checkLatestVersion, localPackageVersion, isDevInstall } from '../runtim
19
18
  import { spawnStagedInstall, runStagedInstall, isStagedComplete } from '../runtime/shared/staged-update.mjs';
20
19
  import {
21
20
  modelVisibleToolCompletionMessage,
22
- shouldPersistModelVisibleToolCompletion,
23
21
  } from '../runtime/shared/tool-execution-contract.mjs';
24
22
  import {
25
23
  channelNotificationModelContent,
@@ -120,7 +118,6 @@ import {
120
118
  fastCapableFor,
121
119
  makeSearchCapableFor,
122
120
  fastPreferenceFor,
123
- saveModelSettings,
124
121
  } from './model-capabilities.mjs';
125
122
  import {
126
123
  DEFAULT_PROVIDER,
@@ -248,6 +245,35 @@ import { createResourceApi } from './resource-api.mjs';
248
245
  import { createModelRouteApi } from './model-route-api.mjs';
249
246
  import { createWorkflowAgentsApi } from './workflow-agents-api.mjs';
250
247
  import { createSessionTurnApi } from './session-turn-api.mjs';
248
+ import { providerInitCacheKey } from './provider-init-key.mjs';
249
+ import {
250
+ RUNTIME,
251
+ SEARCH_RUNTIME,
252
+ SEARCH_TOOL_DEFS,
253
+ MEMORY_TOOL_DEFS,
254
+ MEMORY_RUNTIME,
255
+ CHANNEL_TOOL_DEFS,
256
+ CHANNEL_WORKER_ENTRY,
257
+ CODE_GRAPH_TOOL_DEFS,
258
+ CODE_GRAPH_RUNTIME,
259
+ STATUSLINE_SESSION_ROUTES,
260
+ SESSION_RUNTIME_DIR,
261
+ STANDALONE_SOURCE_ROOT,
262
+ STANDALONE_ROOT,
263
+ STANDALONE_DATA_DIR,
264
+ } from './runtime-paths.mjs';
265
+ import {
266
+ dispatchSearchRuntimeTool,
267
+ memoryToolArgsForCaller,
268
+ shouldMirrorCompletionToPendingQueue,
269
+ } from './runtime-tool-routing.mjs';
270
+ export {
271
+ __renderToolSearchForTest,
272
+ __saveModelSettingsForTest,
273
+ dispatchSearchRuntimeTool,
274
+ memoryToolArgsForCaller,
275
+ shouldMirrorCompletionToPendingQueue,
276
+ } from './runtime-tool-routing.mjs';
251
277
  // Re-exported for external consumers (scripts/tool-smoke.mjs) that imported
252
278
  // these from this module before the tool-defs extraction.
253
279
  export { TOOL_SEARCH_TOOL, SKILL_TOOL };
@@ -256,26 +282,6 @@ export function __applyStandaloneToolDefaultsForTest(tool) {
256
282
  return applyStandaloneToolDefaults(tool);
257
283
  }
258
284
 
259
- const RUNTIME = '../runtime/agent/orchestrator';
260
- const SEARCH_RUNTIME = '../runtime/search/index.mjs';
261
- const SEARCH_TOOL_DEFS = '../runtime/search/tool-defs.mjs';
262
- const MEMORY_TOOL_DEFS = '../runtime/memory/tool-defs.mjs';
263
- const MEMORY_RUNTIME = '../runtime/memory/index.mjs';
264
- const CHANNEL_TOOL_DEFS = '../runtime/channels/tool-defs.mjs';
265
- const CHANNEL_WORKER_ENTRY = '../runtime/channels/index.mjs';
266
- const CODE_GRAPH_TOOL_DEFS = '../runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs';
267
- const CODE_GRAPH_RUNTIME = '../runtime/agent/orchestrator/tools/code-graph.mjs';
268
- const STATUSLINE_SESSION_ROUTES = '../vendor/statusline/src/gateway/session-routes.mjs';
269
- const __dirname = dirname(fileURLToPath(import.meta.url));
270
- // This module lives in src/session-runtime/, but the resource root must remain
271
- // src/ (defaults/, rules/, runtime/, vendor/ live there), so climb one level.
272
- const STANDALONE_SOURCE_ROOT = dirname(__dirname);
273
- // Resource root stays at src/ because defaults/, rules/, runtime/, vendor/ live
274
- // there. User-owned standalone state lives under MIXDOG_HOME (~/.mixdog).
275
- const STANDALONE_ROOT = STANDALONE_SOURCE_ROOT;
276
- const MIXDOG_HOME = process.env.MIXDOG_HOME || join(homedir(), '.mixdog');
277
- const STANDALONE_DATA_DIR = process.env.MIXDOG_DATA_DIR || join(MIXDOG_HOME, 'data');
278
-
279
285
  const resolveDefaultProvider = makeResolveDefaultProvider(isKnownProvider);
280
286
  const resolveRoute = makeResolveRoute(resolveDefaultProvider);
281
287
  const searchCapableFor = makeSearchCapableFor(normalizeSearchProviderId, isSearchCapableProvider);
@@ -303,68 +309,15 @@ const {
303
309
  agentRouteFromConfig,
304
310
  } = createWorkflowRouteHelpers({ resolveDefaultProvider, findPreset });
305
311
 
306
- export function __renderToolSearchForTest(args = {}, session = {}, mode = 'full', options = {}) {
307
- return renderToolSearch(args, session, mode, options);
308
- }
309
-
310
- export function __saveModelSettingsForTest(cfgMod, route, options = {}) {
311
- return saveModelSettings(cfgMod, route, options);
312
- }
313
-
314
- // Decide whether notifyFnForSession should mirror a terminal tool completion
315
- // into the pending-message queue. Only the TUI execution-ui path actually
316
- // injects the model-visible twin of the completion into the active loop, and it
317
- // signals that with an EXPLICIT ack (modelVisibleDelivered) — never a generic
318
- // truthy onNotification return. Skipping the mirror on a bare `handled===true`
319
- // would let a display-only / API listener suppress the sole model-visible copy,
320
- // so the model would never see the completion body. Mirror UNLESS the body was
321
- // explicitly delivered; unknown/generic-handled → keep the mirror.
322
- export function shouldMirrorCompletionToPendingQueue({
323
- callerSessionId,
324
- modelVisibleDelivered,
325
- hasEnqueue,
326
- text,
327
- meta = {},
328
- } = {}) {
329
- if (!callerSessionId || !hasEnqueue) return false;
330
- if (modelVisibleDelivered) return false;
331
- return shouldPersistModelVisibleToolCompletion(text, meta);
332
- }
333
-
334
- export async function dispatchSearchRuntimeTool(name, args, callerCtx = {}, {
335
- getSearchModule,
336
- getCurrentCwd,
337
- getSession,
338
- notifyFnForSession,
339
- runNativeWebSearch,
340
- } = {}) {
341
- const currentSession = typeof getSession === 'function' ? getSession() : null;
342
- const callerCwd = callerCtx?.callerCwd || (typeof getCurrentCwd === 'function' ? getCurrentCwd() : process.cwd());
343
- const callerSessionId = callerCtx?.callerSessionId || currentSession?.id || null;
344
- const callerSignal = callerCtx?.signal || currentSession?.controller?.signal;
345
- const searchMod = await getSearchModule();
346
- if (!searchMod?.handleToolCall) throw new Error('search runtime is not available');
347
- return await searchMod.handleToolCall(name, args || {}, {
348
- callerCwd,
349
- callerSessionId,
350
- routingSessionId: callerSessionId,
351
- clientHostPid: callerCtx?.clientHostPid || currentSession?.clientHostPid || process.pid,
352
- notifyFn: notifyFnForSession(callerSessionId),
353
- signal: callerSignal,
354
- nativeSearch: name === 'search'
355
- ? async (searchArgs) => runNativeWebSearch(searchArgs, { signal: callerSignal })
356
- : undefined,
357
- });
358
- }
359
-
360
312
  export async function createMixdogSessionRuntime({
361
313
  provider,
362
314
  model,
363
315
  cwd = process.cwd(),
364
316
  toolMode = 'full',
365
317
  remote = false,
366
- desktopSession = null,
318
+ desktopSession: initialDesktopSession = null,
367
319
  } = {}) {
320
+ let desktopSession = initialDesktopSession;
368
321
  bootProfile('session-runtime:start', { provider, model, toolMode, cwd });
369
322
  let remoteEnabled = remote === true;
370
323
  // Transient marker: an AUTO start (config/delayed autoStart) has forked the
@@ -434,7 +387,7 @@ export async function createMixdogSessionRuntime({
434
387
  const memoryRuntime = createStandaloneMemoryRuntime({
435
388
  // Entry constants are module-relative ('../runtime/...'); resolve against
436
389
  // this module's dir, not STANDALONE_ROOT, or the 'src/' segment is lost.
437
- entry: join(__dirname, MEMORY_RUNTIME),
390
+ entry: join(SESSION_RUNTIME_DIR, MEMORY_RUNTIME),
438
391
  dataDir: pluginDataDir,
439
392
  cwd,
440
393
  });
@@ -656,6 +609,8 @@ export async function createMixdogSessionRuntime({
656
609
  mcpClient,
657
610
  getConfig: () => config,
658
611
  getCurrentCwd: () => currentCwd,
612
+ getDesktopSession: () => desktopSession,
613
+ setDesktopSession: (v) => { desktopSession = v; },
659
614
  state: mcpState,
660
615
  });
661
616
  let preSessionToolSurface = null;
@@ -1031,7 +986,7 @@ export async function createMixdogSessionRuntime({
1031
986
  };
1032
987
  const channelsStartedAt = performance.now();
1033
988
  const channels = createStandaloneChannelWorker({
1034
- entry: join(__dirname, CHANNEL_WORKER_ENTRY),
989
+ entry: join(SESSION_RUNTIME_DIR, CHANNEL_WORKER_ENTRY),
1035
990
  rootDir: STANDALONE_ROOT,
1036
991
  dataDir: cfgMod.getPluginData(),
1037
992
  cwd,
@@ -1146,7 +1101,7 @@ export async function createMixdogSessionRuntime({
1146
1101
  if (name === 'recall' || name === 'memory' || name === 'search_memories') {
1147
1102
  const memoryMod = await getMemoryModule();
1148
1103
  if (!memoryMod?.handleToolCall) throw new Error('memory runtime is not available');
1149
- return await memoryMod.handleToolCall(name, args || {});
1104
+ return await memoryMod.handleToolCall(name, memoryToolArgsForCaller(args, callerCwd));
1150
1105
  }
1151
1106
  if (name === 'code_graph') {
1152
1107
  const codeGraphMod = await getCodeGraphModule();
@@ -1267,6 +1222,7 @@ export async function createMixdogSessionRuntime({
1267
1222
  flushSkillsSave,
1268
1223
  flushOutputStyleSave,
1269
1224
  scheduleOutputStyleSave,
1225
+ flushAllConfigSavesAsync,
1270
1226
  reloadFullConfig,
1271
1227
  ensureFullConfig,
1272
1228
  displayConfig,
@@ -1293,23 +1249,6 @@ export async function createMixdogSessionRuntime({
1293
1249
  STANDALONE_DATA_DIR,
1294
1250
  });
1295
1251
 
1296
- function providerInitCacheKey(value) {
1297
- function sortKeysDeep(v) {
1298
- if (Array.isArray(v)) return v.map(sortKeysDeep);
1299
- if (v && typeof v === 'object') {
1300
- const out = {};
1301
- for (const key of Object.keys(v).sort()) out[key] = sortKeysDeep(v[key]);
1302
- return out;
1303
- }
1304
- return v;
1305
- }
1306
- try {
1307
- return JSON.stringify(sortKeysDeep(value));
1308
- } catch {
1309
- return `uncacheable:${Date.now()}:${Math.random()}`;
1310
- }
1311
- }
1312
-
1313
1252
  async function ensureProvidersReady(providerConfig = config.providers || {}) {
1314
1253
  const initKey = providerInitCacheKey(providerConfig);
1315
1254
  const existing = providerInitPromises.get(initKey);
@@ -1879,10 +1818,6 @@ export async function createMixdogSessionRuntime({
1879
1818
  // No-op when the session has not been created yet (lazy mode); that
1880
1819
  // case is covered by the turn-start rebind in ask().
1881
1820
  ensureRemoteTranscriptWriter();
1882
- // A backend switch may still be sitting in its debounce window; flush it
1883
- // so the channel worker boots against the backend the user just chose,
1884
- // not the previous on-disk value.
1885
- try { flushBackendSave(); } catch {}
1886
1821
  if (envFlag('MIXDOG_DISABLE_CHANNEL_START')) {
1887
1822
  bootProfile('channels:start-skipped');
1888
1823
  return true;
@@ -1898,6 +1833,9 @@ export async function createMixdogSessionRuntime({
1898
1833
  }
1899
1834
  bootProfile('channels:start-scheduled', { delayMs: 0, immediate: true });
1900
1835
  void (async () => {
1836
+ // A backend switch may still be pending or writing asynchronously. Drain
1837
+ // it before the worker reads config; never race it with a sync lock wait.
1838
+ try { await flushBackendSave(); } catch {}
1901
1839
  // Yield before the createCurrentSession/transcript/fork chain below —
1902
1840
  // same rationale as the memory-eager-init yield above: this detached
1903
1841
  // chain runs synchronous config/fs work (createCurrentSession, backend
@@ -2130,6 +2068,8 @@ export async function createMixdogSessionRuntime({
2130
2068
  getConfig: () => config,
2131
2069
  getMode: () => mode,
2132
2070
  getCurrentCwd: () => currentCwd,
2071
+ getDesktopSession: () => desktopSession,
2072
+ setDesktopSession: (v) => { desktopSession = v; },
2133
2073
  setCloseRequested: (v) => { closeRequested = v; },
2134
2074
  getMemoryModPromise: () => memoryModPromise,
2135
2075
  setMemoryModPromise: (v) => { memoryModPromise = v; },
@@ -2147,6 +2087,7 @@ export async function createMixdogSessionRuntime({
2147
2087
  flushConfigSave,
2148
2088
  flushBackendSave,
2149
2089
  flushOutputStyleSave,
2090
+ flushAllConfigSavesAsync,
2150
2091
  withTeardownDeadline,
2151
2092
  closePatchRuntimeIfLoaded,
2152
2093
  createCurrentSession,
@@ -0,0 +1,20 @@
1
+ import { homedir } from 'node:os';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ export const RUNTIME = '../runtime/agent/orchestrator';
6
+ export const SEARCH_RUNTIME = '../runtime/search/index.mjs';
7
+ export const SEARCH_TOOL_DEFS = '../runtime/search/tool-defs.mjs';
8
+ export const MEMORY_TOOL_DEFS = '../runtime/memory/tool-defs.mjs';
9
+ export const MEMORY_RUNTIME = '../runtime/memory/index.mjs';
10
+ export const CHANNEL_TOOL_DEFS = '../runtime/channels/tool-defs.mjs';
11
+ export const CHANNEL_WORKER_ENTRY = '../runtime/channels/index.mjs';
12
+ export const CODE_GRAPH_TOOL_DEFS = '../runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs';
13
+ export const CODE_GRAPH_RUNTIME = '../runtime/agent/orchestrator/tools/code-graph.mjs';
14
+ export const STATUSLINE_SESSION_ROUTES = '../vendor/statusline/src/gateway/session-routes.mjs';
15
+
16
+ export const SESSION_RUNTIME_DIR = dirname(fileURLToPath(import.meta.url));
17
+ export const STANDALONE_SOURCE_ROOT = dirname(SESSION_RUNTIME_DIR);
18
+ export const STANDALONE_ROOT = STANDALONE_SOURCE_ROOT;
19
+ const mixdogHome = process.env.MIXDOG_HOME || join(homedir(), '.mixdog');
20
+ export const STANDALONE_DATA_DIR = process.env.MIXDOG_DATA_DIR || join(mixdogHome, 'data');
@@ -0,0 +1,55 @@
1
+ import { shouldPersistModelVisibleToolCompletion } from '../runtime/shared/tool-execution-contract.mjs';
2
+ import { saveModelSettings } from './model-capabilities.mjs';
3
+ import { renderToolSearch } from './tool-catalog.mjs';
4
+
5
+ export function __renderToolSearchForTest(args = {}, session = {}, mode = 'full', options = {}) {
6
+ return renderToolSearch(args, session, mode, options);
7
+ }
8
+
9
+ export function __saveModelSettingsForTest(cfgMod, route, options = {}) {
10
+ return saveModelSettings(cfgMod, route, options);
11
+ }
12
+
13
+ export function shouldMirrorCompletionToPendingQueue({
14
+ callerSessionId,
15
+ modelVisibleDelivered,
16
+ hasEnqueue,
17
+ text,
18
+ meta = {},
19
+ } = {}) {
20
+ if (!callerSessionId || !hasEnqueue || modelVisibleDelivered) return false;
21
+ return shouldPersistModelVisibleToolCompletion(text, meta);
22
+ }
23
+
24
+ export async function dispatchSearchRuntimeTool(name, args, callerCtx = {}, {
25
+ getSearchModule,
26
+ getCurrentCwd,
27
+ getSession,
28
+ notifyFnForSession,
29
+ runNativeWebSearch,
30
+ } = {}) {
31
+ const currentSession = typeof getSession === 'function' ? getSession() : null;
32
+ const callerCwd = callerCtx?.callerCwd || (typeof getCurrentCwd === 'function' ? getCurrentCwd() : process.cwd());
33
+ const callerSessionId = callerCtx?.callerSessionId || currentSession?.id || null;
34
+ const callerSignal = callerCtx?.signal || currentSession?.controller?.signal;
35
+ const searchMod = await getSearchModule();
36
+ if (!searchMod?.handleToolCall) throw new Error('search runtime is not available');
37
+ return await searchMod.handleToolCall(name, args || {}, {
38
+ callerCwd,
39
+ callerSessionId,
40
+ routingSessionId: callerSessionId,
41
+ clientHostPid: callerCtx?.clientHostPid || currentSession?.clientHostPid || process.pid,
42
+ notifyFn: notifyFnForSession(callerSessionId),
43
+ signal: callerSignal,
44
+ nativeSearch: name === 'search'
45
+ ? async (searchArgs) => runNativeWebSearch(searchArgs, { signal: callerSignal })
46
+ : undefined,
47
+ });
48
+ }
49
+
50
+ export function memoryToolArgsForCaller(args, callerCwd) {
51
+ const input = args && typeof args === 'object' && !Array.isArray(args) ? args : {};
52
+ return typeof input.cwd === 'string' && input.cwd.trim()
53
+ ? input
54
+ : { ...input, cwd: callerCwd };
55
+ }
@@ -0,0 +1,51 @@
1
+ export const MEASURED_TOOL_USAGE = Object.freeze({
2
+ read: 710,
3
+ code_graph: 520,
4
+ grep: 500,
5
+ find: 480,
6
+ glob: 460,
7
+ list: 430,
8
+ apply_patch: 400,
9
+ explore: 360,
10
+ agent: 330,
11
+ shell: 81,
12
+ cwd: 2,
13
+ recall: 2,
14
+ search: 2,
15
+ web_fetch: 2,
16
+ });
17
+ export const MEASURED_TOOL_ORDER = Object.freeze(Object.keys(MEASURED_TOOL_USAGE));
18
+
19
+ export const DEFERRED_DEFAULT_FULL_TOOLS = Object.freeze([
20
+ 'read', 'code_graph', 'grep', 'find', 'glob', 'list', 'explore',
21
+ 'apply_patch', 'Skill', 'load_tool',
22
+ ]);
23
+ export const DEFERRED_DEFAULT_READONLY_TOOLS = Object.freeze([
24
+ 'read', 'code_graph', 'grep', 'find', 'glob', 'list', 'explore',
25
+ 'Skill', 'load_tool',
26
+ ]);
27
+ export const DEFERRED_DEFAULT_LEAD_TOOLS = Object.freeze([
28
+ 'read', 'code_graph', 'grep', 'find', 'glob', 'list', 'shell', 'task',
29
+ 'explore', 'apply_patch', 'agent', 'recall', 'search', 'web_fetch', 'cwd',
30
+ 'session_manage', 'Skill', 'load_tool',
31
+ ]);
32
+
33
+ export const READONLY_TOOL_NAMES = new Set([
34
+ 'read', 'list', 'grep', 'find', 'glob', 'code_graph', 'search',
35
+ 'web_fetch', 'recall', 'memory', 'fetch', 'Skill',
36
+ ]);
37
+
38
+ export const DEFERRED_SELECT_ALIASES = {
39
+ filesystem: ['read', 'list', 'grep', 'find', 'glob'],
40
+ search: ['search', 'web_fetch'],
41
+ web: ['web_fetch', 'search'],
42
+ memory: ['memory', 'recall'],
43
+ channels: ['reply', 'fetch'],
44
+ discord: ['reply', 'fetch'],
45
+ explore: ['explore'],
46
+ discovery: ['explore'],
47
+ agent: ['agent'],
48
+ graph: ['code_graph'],
49
+ code: ['code_graph'],
50
+ shell: ['shell', 'task'],
51
+ };
@@ -17,24 +17,22 @@ import {
17
17
  finalizeProviderRequestTools,
18
18
  providerNativeToolPrefixCount,
19
19
  } from './provider-request-tools.mjs';
20
+ import {
21
+ DEFERRED_DEFAULT_FULL_TOOLS,
22
+ DEFERRED_DEFAULT_LEAD_TOOLS,
23
+ DEFERRED_DEFAULT_READONLY_TOOLS,
24
+ DEFERRED_SELECT_ALIASES,
25
+ MEASURED_TOOL_ORDER,
26
+ MEASURED_TOOL_USAGE,
27
+ READONLY_TOOL_NAMES,
28
+ } from './tool-catalog-data.mjs';
29
+ export {
30
+ DEFERRED_DEFAULT_FULL_TOOLS,
31
+ DEFERRED_DEFAULT_LEAD_TOOLS,
32
+ DEFERRED_DEFAULT_READONLY_TOOLS,
33
+ MEASURED_TOOL_USAGE,
34
+ } from './tool-catalog-data.mjs';
20
35
 
21
- export const MEASURED_TOOL_USAGE = Object.freeze({
22
- read: 710,
23
- code_graph: 520,
24
- grep: 500,
25
- find: 480,
26
- glob: 460,
27
- list: 430,
28
- apply_patch: 400,
29
- explore: 360,
30
- agent: 330,
31
- shell: 81,
32
- cwd: 2,
33
- recall: 2,
34
- search: 2,
35
- web_fetch: 2,
36
- });
37
- const MEASURED_TOOL_ORDER = Object.freeze(Object.keys(MEASURED_TOOL_USAGE));
38
36
  const toolSchemaBreakdownMemo = new WeakMap();
39
37
 
40
38
  function sameToolSchemaEntries(cached, tools) {
@@ -81,78 +79,6 @@ function toolSchemaEntry(tool, native = false) {
81
79
  wireSignature: toolSchemaSignature(toolMeteringList(tool, native)),
82
80
  };
83
81
  }
84
- export const DEFERRED_DEFAULT_FULL_TOOLS = Object.freeze([
85
- 'read',
86
- 'code_graph',
87
- 'grep',
88
- 'find',
89
- 'glob',
90
- 'list',
91
- 'explore',
92
- 'apply_patch',
93
- 'Skill',
94
- 'load_tool',
95
- ]);
96
- export const DEFERRED_DEFAULT_READONLY_TOOLS = Object.freeze([
97
- 'read',
98
- 'code_graph',
99
- 'grep',
100
- 'find',
101
- 'glob',
102
- 'list',
103
- 'explore',
104
- 'Skill',
105
- 'load_tool',
106
- ]);
107
- export const DEFERRED_DEFAULT_LEAD_TOOLS = Object.freeze([
108
- 'read',
109
- 'code_graph',
110
- 'grep',
111
- 'find',
112
- 'glob',
113
- 'list',
114
- 'shell',
115
- 'task',
116
- 'explore',
117
- 'apply_patch',
118
- 'agent',
119
- 'recall',
120
- 'search',
121
- 'web_fetch',
122
- 'cwd',
123
- 'session_manage',
124
- 'Skill',
125
- 'load_tool',
126
- ]);
127
- const READONLY_TOOL_NAMES = new Set([
128
- 'read',
129
- 'list',
130
- 'grep',
131
- 'find',
132
- 'glob',
133
- 'code_graph',
134
- 'search',
135
- 'web_fetch',
136
- 'recall',
137
- 'memory',
138
- 'fetch',
139
- 'Skill',
140
- ]);
141
- const DEFERRED_SELECT_ALIASES = {
142
- filesystem: ['read', 'list', 'grep', 'find', 'glob'],
143
- search: ['search', 'web_fetch'],
144
- web: ['web_fetch', 'search'],
145
- memory: ['memory', 'recall'],
146
- channels: ['reply', 'fetch'],
147
- discord: ['reply', 'fetch'],
148
- explore: ['explore'],
149
- discovery: ['explore'],
150
- agent: ['agent'],
151
- graph: ['code_graph'],
152
- code: ['code_graph'],
153
- shell: ['shell', 'task'],
154
- };
155
-
156
82
  export function toolKind(tool) {
157
83
  const name = clean(tool?.name);
158
84
  if (name.startsWith('mcp__')) return 'mcp';