mixdog 0.9.85 → 0.9.87

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 (102) hide show
  1. package/README.md +7 -1
  2. package/package.json +4 -3
  3. package/src/defaults/agents.json +12 -0
  4. package/src/defaults/skills/setup/SKILL.md +44 -12
  5. package/src/help.mjs +40 -9
  6. package/src/lib/keychain-cjs.cjs +11 -1
  7. package/src/rules/agent/43-title-agent.md +22 -0
  8. package/src/rules/shared/01-tool.md +3 -3
  9. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -2
  10. package/src/runtime/agent/orchestrator/mcp/client.mjs +24 -0
  11. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +84 -3
  12. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +125 -16
  13. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +2 -2
  14. package/src/runtime/agent/orchestrator/session/context-utils.mjs +73 -45
  15. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +15 -18
  16. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -0
  18. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +135 -9
  19. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +31 -12
  20. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +4 -0
  21. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -0
  22. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +170 -0
  23. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +45 -6
  24. package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
  25. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +26 -0
  26. package/src/runtime/agent/orchestrator/session/token-bpe.mjs +42 -0
  27. package/src/runtime/agent/orchestrator/session/token-native.mjs +186 -0
  28. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +15 -5
  29. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +20 -0
  30. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -2
  31. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +24 -0
  32. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +8 -0
  33. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  34. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +286 -0
  35. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  36. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -3
  37. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +152 -18
  38. package/src/runtime/agent/orchestrator/tools/token-binary-fetcher.mjs +201 -0
  39. package/src/runtime/agent/orchestrator/tools/token-manifest.json +26 -0
  40. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +3 -3
  41. package/src/runtime/media/adapters/gemini-video.mjs +1 -1
  42. package/src/runtime/media/renditions.mjs +1 -1
  43. package/src/runtime/media/store.mjs +1 -1
  44. package/src/runtime/memory/lib/memory-action-handlers.mjs +4 -1
  45. package/src/runtime/memory/tool-defs.mjs +19 -3
  46. package/src/runtime/shared/atomic-file.mjs +25 -14
  47. package/src/runtime/shared/automation-attachments.mjs +3 -3
  48. package/src/runtime/shared/child-guardian.mjs +15 -5
  49. package/src/runtime/shared/child-spawn-gate.mjs +6 -12
  50. package/src/runtime/shared/resource-admission.mjs +7 -3
  51. package/src/runtime/shared/turn-snapshot.mjs +0 -3
  52. package/src/session-runtime/lifecycle-api.mjs +49 -9
  53. package/src/session-runtime/prewarm.mjs +35 -0
  54. package/src/session-runtime/provider-auth-api.mjs +8 -0
  55. package/src/session-runtime/provider-usage.mjs +30 -5
  56. package/src/session-runtime/remote-control.mjs +8 -47
  57. package/src/session-runtime/remote-transcript.mjs +3 -7
  58. package/src/session-runtime/route-preparation.mjs +24 -0
  59. package/src/session-runtime/runtime-core.mjs +286 -548
  60. package/src/session-runtime/session-lifecycle.mjs +378 -0
  61. package/src/session-runtime/session-turn-api.mjs +9 -2
  62. package/src/session-runtime/workflow-agents-api.mjs +3 -0
  63. package/src/standalone/agent-shard/shard-child.mjs +300 -0
  64. package/src/standalone/agent-shard/shard-pool.mjs +443 -0
  65. package/src/standalone/agent-tool/job-views.mjs +329 -0
  66. package/src/standalone/agent-tool/spawn-flow.mjs +629 -0
  67. package/src/standalone/agent-tool/tag-registry.mjs +340 -0
  68. package/src/standalone/agent-tool.mjs +112 -941
  69. package/src/standalone/channel-daemon-transport.mjs +126 -185
  70. package/src/standalone/channel-daemon.mjs +9 -10
  71. package/src/standalone/usage-dashboard.mjs +23 -1
  72. package/src/tui/App.jsx +362 -2569
  73. package/src/tui/app/app-view.jsx +504 -0
  74. package/src/tui/app/channel-pickers.mjs +3 -8
  75. package/src/tui/app/create-app-pickers.mjs +313 -0
  76. package/src/tui/app/prompt-submit.mjs +501 -0
  77. package/src/tui/app/route-pickers.mjs +117 -152
  78. package/src/tui/app/settings-picker.mjs +90 -89
  79. package/src/tui/app/shell-layout.mjs +563 -0
  80. package/src/tui/app/slash-commands.mjs +7 -7
  81. package/src/tui/app/slash-dispatch.mjs +0 -45
  82. package/src/tui/app/usage-context-panels.mjs +288 -0
  83. package/src/tui/app/use-copy-selection.mjs +56 -0
  84. package/src/tui/app/use-global-key-input.mjs +151 -0
  85. package/src/tui/app/use-pasted-buffers.mjs +115 -0
  86. package/src/tui/app/use-prompt-draft-flow.mjs +167 -0
  87. package/src/tui/app/use-prompt-hint.mjs +58 -0
  88. package/src/tui/app/use-prompt-queue-history.mjs +100 -0
  89. package/src/tui/app/use-terminal-chrome.mjs +72 -0
  90. package/src/tui/app/use-transcript-activity.mjs +131 -0
  91. package/src/tui/app/use-welcome-prompt-hint.mjs +96 -0
  92. package/src/tui/components/StatusLine.jsx +1 -1
  93. package/src/tui/components/tool-execution/ResultBody.jsx +1 -1
  94. package/src/tui/dist/index.mjs +9857 -9159
  95. package/src/tui/engine/session-api-ext.mjs +47 -9
  96. package/src/tui/engine.mjs +7 -1
  97. package/src/tui/figures.mjs +0 -1
  98. package/src/ui/statusline-format.mjs +0 -1
  99. package/src/hooks/lib/permission-evaluator.cjs +0 -24
  100. package/src/lib/config-cjs.cjs +0 -61
  101. package/src/runtime/shared/launcher-control.mjs +0 -258
  102. package/src/runtime/shared/workspace-router.mjs +0 -259
@@ -1,3 +1,4 @@
1
+ import { createSessionLifecycle } from './session-lifecycle.mjs';
1
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
2
3
  import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
3
4
  import { performance } from 'node:perf_hooks';
@@ -41,7 +42,10 @@ import {
41
42
  setLocalProvider,
42
43
  } from '../standalone/provider-admin.mjs';
43
44
  import { createUsageDashboard } from '../standalone/usage-dashboard.mjs';
44
- import { fetchOAuthUsageSnapshot } from '../runtime/agent/orchestrator/providers/oauth-usage.mjs';
45
+ import {
46
+ consumeOpenAICodexResetCredit,
47
+ fetchOAuthUsageSnapshot,
48
+ } from '../runtime/agent/orchestrator/providers/oauth-usage.mjs';
45
49
  import {
46
50
  getModelMetadataSync,
47
51
  warmCatalogsInBackground,
@@ -246,6 +250,7 @@ import { createToolSurface } from './tool-surface.mjs';
246
250
  import { readRuntimeTunables } from './runtime-tunables.mjs';
247
251
  import { createSessionTurnApi } from './session-turn-api.mjs';
248
252
  import { providerInitCacheKey } from './provider-init-key.mjs';
253
+ import { createRoutePreparationGate } from './route-preparation.mjs';
249
254
  import {
250
255
  RUNTIME,
251
256
  SEARCH_RUNTIME,
@@ -319,28 +324,22 @@ export async function createMixdogSessionRuntime({
319
324
  remote = false,
320
325
  desktopSession: initialDesktopSession = null,
321
326
  } = {}) {
322
- let desktopSession = initialDesktopSession;
327
+ // Shared mutable runtime state, promoted from closure `let`s so extracted
328
+ // modules can read/write live values through one reference.
329
+ const rt = {};
330
+ rt.desktopSession = initialDesktopSession;
323
331
  bootProfile('session-runtime:start', { provider, model, toolMode, cwd });
324
- let remoteEnabled = remote === true;
325
- // Transient marker: an AUTO start (config/delayed autoStart) has forked the
326
- // worker to ATTEMPT a claim-if-vacant but has NOT asserted remote for this
327
- // session yet. remoteEnabled flips only if the worker reports it acquired the
328
- // seat (the 'acquired' notification). Lets the deferred start chain proceed
329
- // past its remoteEnabled guards without prematurely showing this session as
330
- // remote (single-holder: a live owner must not be stolen by autoStart).
331
- let remoteClaimPending = false;
332
- // SESSION-SCOPED remote (user decision): the session that toggled remote
333
- // OWNS the channel relay. Other sessions' turns never rebind or relay; a
334
- // dead/replaced owner (clear, delete) hands the seat to the next current
335
- // session that asks.
336
- let remoteSessionId = null;
332
+ rt.remoteEnabled = remote === true;
333
+ // SESSION-SCOPED Remote is manual-only. Explicit ON selects/overrides; no
334
+ // registration, turn, session death, or process death transfers ownership.
335
+ rt.remoteSessionId = null;
337
336
  // Desktop/TUI Remote toggles are desired-state transitions. Serialize the
338
337
  // daemon deactivate/detach and reconnect/activate chains so an OFF that is
339
338
  // still stopping can never tear down the worker created by the following ON.
340
339
  const remoteTransitions = createRemoteTransitionQueue();
341
340
  // Last assistant text handed to the transcript writer (via onAssistantText),
342
341
  // so the post-turn final-content append can skip an exact duplicate.
343
- let _lastAppendedAssistant = '';
342
+ rt._lastAppendedAssistant = '';
344
343
  process.env.MIXDOG_QUIET_SESSION_LOG ??= '1';
345
344
  const standaloneStartedAt = performance.now();
346
345
  ensureStandaloneEnvironment({
@@ -349,11 +348,11 @@ export async function createMixdogSessionRuntime({
349
348
  });
350
349
  bootProfile('standalone-env:ready', { ms: (performance.now() - standaloneStartedAt).toFixed(1) });
351
350
  const keychainPrewarmPromise = keychain.prewarmSecrets();
352
- let keychainPrewarmWaitDone = false;
353
- let keychainPrewarmWaitPromise = null;
351
+ rt.keychainPrewarmWaitDone = false;
352
+ rt.keychainPrewarmWaitPromise = null;
354
353
  function awaitKeychainPrewarm() {
355
- if (keychainPrewarmWaitDone) return Promise.resolve();
356
- keychainPrewarmWaitPromise ??= (async () => {
354
+ if (rt.keychainPrewarmWaitDone) return Promise.resolve();
355
+ rt.keychainPrewarmWaitPromise ??= (async () => {
357
356
  let timeoutId;
358
357
  const deadline = new Promise((resolveDeadline) => {
359
358
  timeoutId = setTimeout(resolveDeadline, KEYCHAIN_PREWARM_WAIT_MS);
@@ -363,14 +362,19 @@ export async function createMixdogSessionRuntime({
363
362
  await Promise.race([keychainPrewarmPromise, deadline]);
364
363
  } finally {
365
364
  clearTimeout(timeoutId);
366
- keychainPrewarmWaitDone = true;
365
+ rt.keychainPrewarmWaitDone = true;
367
366
  }
368
367
  // Invoked here: the assignment used to store the async FUNCTION, so every
369
368
  // `await awaitKeychainPrewarm()` resolved instantly (awaiting a function is
370
369
  // a no-op) and callers silently skipped the wait they asked for.
371
370
  })();
372
- return keychainPrewarmWaitPromise;
371
+ return rt.keychainPrewarmWaitPromise;
373
372
  }
373
+ const routePreparation = createRoutePreparationGate({
374
+ onError: (error) => bootProfile('route-preparation:failed', {
375
+ error: error?.message || String(error),
376
+ }),
377
+ });
374
378
 
375
379
  const importsStartedAt = performance.now();
376
380
  const [
@@ -416,22 +420,22 @@ export async function createMixdogSessionRuntime({
416
420
  dataDir: pluginDataDir,
417
421
  cwd,
418
422
  });
419
- let memoryModPromise = null;
420
- let searchModPromise = null;
421
- let codeGraphModPromise = null;
423
+ rt.memoryModPromise = null;
424
+ rt.searchModPromise = null;
425
+ rt.codeGraphModPromise = null;
422
426
 
423
427
  // Memory module is always-on. `memoryEnabled()` is kept as a thin alias that
424
428
  // now always returns true (callers/compaction helpers still reference it);
425
429
  // the user-facing toggle is `recap` (background cycles only), read via
426
430
  // recapEnabled(config).
427
431
  const memoryEnabled = () => true;
428
- const recapEnabledFn = () => recapEnabled(config, true);
429
- const channelsEnabled = () => moduleEnabled(config, 'channels', true);
432
+ const recapEnabledFn = () => recapEnabled(rt.config, true);
433
+ const channelsEnabled = () => moduleEnabled(rt.config, 'channels', true);
430
434
 
431
435
  async function getMemoryModule() {
432
436
  const startedAt = performance.now();
433
- memoryModPromise ??= Promise.resolve(memoryRuntime);
434
- const mod = await memoryModPromise;
437
+ rt.memoryModPromise ??= Promise.resolve(memoryRuntime);
438
+ const mod = await rt.memoryModPromise;
435
439
  if (typeof mod?.init === 'function') {
436
440
  await mod.init();
437
441
  }
@@ -441,16 +445,16 @@ export async function createMixdogSessionRuntime({
441
445
 
442
446
  async function getSearchModule() {
443
447
  const startedAt = performance.now();
444
- searchModPromise ??= import(SEARCH_RUNTIME);
445
- const mod = await searchModPromise;
448
+ rt.searchModPromise ??= import(SEARCH_RUNTIME);
449
+ const mod = await rt.searchModPromise;
446
450
  bootProfile('search-runtime:ready', { ms: (performance.now() - startedAt).toFixed(1) });
447
451
  return mod;
448
452
  }
449
453
 
450
454
  async function getCodeGraphModule() {
451
455
  const startedAt = performance.now();
452
- codeGraphModPromise ??= import(CODE_GRAPH_RUNTIME);
453
- const mod = await codeGraphModPromise;
456
+ rt.codeGraphModPromise ??= import(CODE_GRAPH_RUNTIME);
457
+ const mod = await rt.codeGraphModPromise;
454
458
  bootProfile('code-graph-runtime:ready', { ms: (performance.now() - startedAt).toFixed(1) });
455
459
  return mod;
456
460
  }
@@ -459,7 +463,7 @@ export async function createMixdogSessionRuntime({
459
463
  const leadRoute = normalizeWorkflowRoute(routeLike);
460
464
  if (!leadRoute) return null;
461
465
 
462
- const nextConfig = { ...(config || {}) };
466
+ const nextConfig = { ...(rt.config || {}) };
463
467
  nextConfig.presets = upsertWorkflowPreset(nextConfig.presets, 'lead', leadRoute);
464
468
  nextConfig.workflowRoutes = {
465
469
  ...(nextConfig.workflowRoutes || {}),
@@ -486,21 +490,21 @@ export async function createMixdogSessionRuntime({
486
490
  }
487
491
 
488
492
  const configStartedAt = performance.now();
489
- let config = cfgMod.loadConfig({ secrets: false });
490
- setConfiguredShell(normalizeSystemShellConfig(config.shell).command);
491
- let configHasSecrets = false;
492
- let route = resolveRoute(config, { provider, model });
493
- let searchRoute = normalizeSearchRouteConfig(config.searchRoute);
493
+ rt.config = cfgMod.loadConfig({ secrets: false });
494
+ setConfiguredShell(normalizeSystemShellConfig(rt.config.shell).command);
495
+ rt.configHasSecrets = false;
496
+ rt.route = resolveRoute(rt.config, { provider, model });
497
+ rt.searchRoute = normalizeSearchRouteConfig(rt.config.searchRoute);
494
498
  bootProfile('config:ready', { ms: (performance.now() - configStartedAt).toFixed(1) });
495
- let mode = normalizeToolMode(toolMode);
496
- let session = null;
497
- let sessionCreatePromise = null;
498
- let currentCwd = cwd;
499
- let sessionNeedsCwdRefresh = false;
499
+ rt.mode = normalizeToolMode(toolMode);
500
+ rt.session = null;
501
+ rt.sessionCreatePromise = null;
502
+ rt.currentCwd = cwd;
503
+ rt.sessionNeedsCwdRefresh = false;
500
504
  // session_manage tool: reset request scheduled by the model mid-turn,
501
505
  // consumed by the TUI engine at turn end ('clear' | 'compact_clear').
502
- let pendingSessionReset = null;
503
- let closeRequested = false;
506
+ rt.pendingSessionReset = null;
507
+ rt.closeRequested = false;
504
508
  const warmupTimers = {
505
509
  providerSetupWarmupTimer: null,
506
510
  providerWarmupTimer: null,
@@ -521,8 +525,8 @@ export async function createMixdogSessionRuntime({
521
525
  codeGraphPrewarmQueuedCwd: '',
522
526
  channelStartPromise: null,
523
527
  };
524
- let activeTurnCount = 0;
525
- let firstTurnCompleted = false;
528
+ rt.activeTurnCount = 0;
529
+ rt.firstTurnCompleted = false;
526
530
  function hookTranscriptPath(sessionId) {
527
531
  const id = clean(sessionId);
528
532
  if (!id || !/^[A-Za-z0-9_-]+$/.test(id)) return null;
@@ -530,15 +534,15 @@ export async function createMixdogSessionRuntime({
530
534
  return join(dataDir, 'sessions', `${id}.json`);
531
535
  }
532
536
  function hookEffortPayload() {
533
- const level = clean(route.effectiveEffort || route.effort);
537
+ const level = clean(rt.route.effectiveEffort || rt.route.effort);
534
538
  return level ? { level: level.toLowerCase() } : undefined;
535
539
  }
536
540
  function hookCommonPayload(extra = {}) {
537
- const sid = clean(extra.session_id || extra.sessionId || session?.id);
541
+ const sid = clean(extra.session_id || extra.sessionId || rt.session?.id);
538
542
  return {
539
543
  ...(sid ? { session_id: sid, transcript_path: hookTranscriptPath(sid) } : {}),
540
- cwd: currentCwd,
541
- permission_mode: session?.permissionMode || 'default',
544
+ cwd: rt.currentCwd,
545
+ permission_mode: rt.session?.permissionMode || 'default',
542
546
  ...(hookEffortPayload() ? { effort: hookEffortPayload() } : {}),
543
547
  ...extra,
544
548
  };
@@ -563,7 +567,7 @@ export async function createMixdogSessionRuntime({
563
567
  modelCatalogWarmupEnabled,
564
568
  codeGraphPrewarmLazy,
565
569
  } = readRuntimeTunables();
566
- let codeGraphFirstTurnPrewarmDone = false;
570
+ rt.codeGraphFirstTurnPrewarmDone = false;
567
571
  const modelMetaByRoute = new Map();
568
572
  const notificationListeners = new Set();
569
573
  // Remote seat listeners (TUI): fired when remote mode flips outside a direct
@@ -588,11 +592,11 @@ export async function createMixdogSessionRuntime({
588
592
  providerSetupPromise: null,
589
593
  };
590
594
  const providerInitPromises = new Map();
591
- let startupProviderCatalogRefreshStarted = false;
595
+ rt.startupProviderCatalogRefreshStarted = false;
592
596
  // True while the boot-time provider-catalog refresh is in flight: warming a
593
597
  // model cache it is about to invalidate only burns the load twice.
594
- let startupProviderCatalogRefreshPending = false;
595
- let lastProjectMcpKey = null;
598
+ rt.startupProviderCatalogRefreshPending = false;
599
+ rt.lastProjectMcpKey = null;
596
600
  // MCP connect state, owned here so teardown/reconnect paths still observe it;
597
601
  // the mcp-glue factory mutates this object in place (see createMcpGlue).
598
602
  const mcpState = {
@@ -611,15 +615,15 @@ export async function createMixdogSessionRuntime({
611
615
  normalizeMcpServerInput,
612
616
  } = createMcpGlue({
613
617
  mcpClient,
614
- getConfig: () => config,
615
- getCurrentCwd: () => currentCwd,
616
- getDesktopSession: () => desktopSession,
617
- setDesktopSession: (v) => { desktopSession = v; },
618
+ getConfig: () => rt.config,
619
+ getCurrentCwd: () => rt.currentCwd,
620
+ getDesktopSession: () => rt.desktopSession,
621
+ setDesktopSession: (v) => { rt.desktopSession = v; },
618
622
  state: mcpState,
619
623
  });
620
624
  const hooksStartedAt = performance.now();
621
625
  const hooks = createStandaloneHookBus({ dataDir: cfgMod.getPluginData() });
622
- hooks.emit('runtime:start', { cwd: currentCwd, provider: route.provider, model: route.model, toolMode: mode });
626
+ hooks.emit('runtime:start', { cwd: rt.currentCwd, provider: rt.route.provider, model: rt.route.model, toolMode: rt.mode });
623
627
  bootProfile('hooks:ready', { ms: (performance.now() - hooksStartedAt).toFixed(1) });
624
628
 
625
629
  // Self-update: registry check + background staging live in self-update.mjs;
@@ -627,7 +631,7 @@ export async function createMixdogSessionRuntime({
627
631
  // boot check is deferred past this constructor so a hanging registry request
628
632
  // can never delay session boot.
629
633
  const selfUpdate = createSelfUpdateController({
630
- getConfig: () => config,
634
+ getConfig: () => rt.config,
631
635
  getDataDir: () => cfgMod.getPluginData?.() || STANDALONE_DATA_DIR,
632
636
  emitNotification: (...a) => emitRuntimeNotification(...a),
633
637
  });
@@ -650,7 +654,7 @@ export async function createMixdogSessionRuntime({
650
654
  skillContent,
651
655
  skillToolContent,
652
656
  addProjectSkill,
653
- } = createSkillsApi({ contextMod, getCwd: () => currentCwd });
657
+ } = createSkillsApi({ contextMod, getCwd: () => rt.currentCwd });
654
658
 
655
659
  // cwd resolution/apply + plugins-status + core-memory context. Extracted to
656
660
  // session-runtime/cwd-plugins.mjs; the facade keeps ownership of the mutable
@@ -664,17 +668,17 @@ export async function createMixdogSessionRuntime({
664
668
  pluginsStatus,
665
669
  loadCoreMemoryContext,
666
670
  } = createCwdPlugins({
667
- getCurrentCwd: () => currentCwd,
668
- setCurrentCwd: (next) => { currentCwd = next; },
669
- getConfig: () => config,
670
- getSession: () => session,
671
- getRoute: () => route,
672
- getLastProjectMcpKey: () => lastProjectMcpKey,
673
- setLastProjectMcpKey: (next) => { lastProjectMcpKey = next; },
671
+ getCurrentCwd: () => rt.currentCwd,
672
+ setCurrentCwd: (next) => { rt.currentCwd = next; },
673
+ getConfig: () => rt.config,
674
+ getSession: () => rt.session,
675
+ getRoute: () => rt.route,
676
+ getLastProjectMcpKey: () => rt.lastProjectMcpKey,
677
+ setLastProjectMcpKey: (next) => { rt.lastProjectMcpKey = next; },
674
678
  isCodeGraphPrewarmLazy: () => codeGraphPrewarmLazy,
675
- isCodeGraphFirstTurnPrewarmDone: () => codeGraphFirstTurnPrewarmDone,
679
+ isCodeGraphFirstTurnPrewarmDone: () => rt.codeGraphFirstTurnPrewarmDone,
676
680
  getCodeGraphPrewarmDelayMs: () => codeGraphPrewarmDelayMs,
677
- setSessionNeedsCwdRefresh: (next) => { sessionNeedsCwdRefresh = next; },
681
+ setSessionNeedsCwdRefresh: (next) => { rt.sessionNeedsCwdRefresh = next; },
678
682
  connectConfiguredMcp,
679
683
  invalidatePreSessionToolSurface: (...a) => invalidatePreSessionToolSurface(...a),
680
684
  scheduleCodeGraphPrewarm: (...a) => scheduleCodeGraphPrewarm(...a),
@@ -706,7 +710,7 @@ export async function createMixdogSessionRuntime({
706
710
  dataDir: cfgMod.getPluginData(),
707
711
  cwd,
708
712
  awaitKeychainPrewarm,
709
- isKeychainPrewarmReady: () => keychainPrewarmWaitDone,
713
+ isKeychainPrewarmReady: () => rt.keychainPrewarmWaitDone,
710
714
  // SubagentStart/SubagentStop: bridge internal worker spawn/finish to the
711
715
  // standard hook bus. agent_type is passed top-level via hookCommonPayload
712
716
  // (added to hook-bus buildEventPayload passthrough). Best-effort.
@@ -723,7 +727,7 @@ export async function createMixdogSessionRuntime({
723
727
  bootProfile('agent:ready', { ms: (performance.now() - agentToolStartedAt).toFixed(1) });
724
728
  const agentStatusState = () => {
725
729
  try {
726
- const status = agentTool.getStatus?.({ clientHostPid: session?.clientHostPid || process.pid }) || {};
730
+ const status = agentTool.getStatus?.({ clientHostPid: rt.session?.clientHostPid || process.pid }) || {};
727
731
  return {
728
732
  agentWorkers: Array.isArray(status.workers) ? status.workers : [],
729
733
  agentJobs: Array.isArray(status.jobs) ? status.jobs : [],
@@ -744,28 +748,10 @@ export async function createMixdogSessionRuntime({
744
748
  // newer remote session. Drop remote mode entirely on this session (no
745
749
  // handover, no retry) and tell UI listeners so the indicator updates.
746
750
  if (msg?.method === 'notifications/mixdog/remote') {
747
- // Any acquire/supersede verdict resolves an in-flight auto claim attempt.
748
- remoteClaimPending = false;
749
- if (msg?.params?.state === 'superseded' && remoteEnabled) {
751
+ if (msg?.params?.state === 'superseded' && rt.remoteEnabled) {
750
752
  stopRemote('superseded-by-newer-remote-session');
751
753
  emitRemoteStateChange(false, 'superseded');
752
754
  }
753
- // Symmetric acquire: the worker took the bridge seat (boot make-before-
754
- // break or activate). Flip remote ON via the same side-state a user
755
- // /remote toggles — remoteEnabled + transcript writer — but WITHOUT
756
- // re-invoking channel start (the worker is already running; startRemote
757
- // would re-fork/activate). Idempotent: no-op when already enabled.
758
- if (msg?.params?.state === 'acquired' && !remoteEnabled) {
759
- remoteEnabled = true;
760
- if (!remoteSessionId) remoteSessionId = session?.id || null;
761
- ensureRemoteTranscriptWriter();
762
- // Auto-acquire: the worker restored yesterday's transcript from
763
- // persisted status and we just created the CURRENT writer. Push the
764
- // repoint now instead of waiting for the next inbound parent-chain
765
- // steal, so outbound forwarding never tails a stale transcript.
766
- pushTranscriptRebind();
767
- emitRemoteStateChange(true, 'acquired');
768
- }
769
755
  return;
770
756
  }
771
757
  if (msg?.method !== 'notifications/claude/channel') return;
@@ -774,8 +760,8 @@ export async function createMixdogSessionRuntime({
774
760
  const content = channelNotificationModelContent(params);
775
761
  if (!content) return;
776
762
  const { handled } = emitRuntimeNotification(content, meta);
777
- if (!handled && session?.id && shouldMirrorChannelNotificationToPending(meta)) {
778
- try { mgr.enqueuePendingMessage(session.id, content); } catch {}
763
+ if (!handled && rt.session?.id && shouldMirrorChannelNotificationToPending(meta)) {
764
+ try { mgr.enqueuePendingMessage(rt.session.id, content); } catch {}
779
765
  }
780
766
  },
781
767
  });
@@ -811,12 +797,12 @@ export async function createMixdogSessionRuntime({
811
797
  applyPreSessionToolSelection,
812
798
  } = createToolSurface({
813
799
  mgr,
814
- mode,
800
+ mode: rt.mode,
815
801
  standaloneTools,
816
802
  agentToolNames,
817
- getSession: () => session,
818
- getRoute: () => route,
819
- getConfig: () => config,
803
+ getSession: () => rt.session,
804
+ getRoute: () => rt.route,
805
+ getConfig: () => rt.config,
820
806
  cfgMod,
821
807
  loadWorkflowPack,
822
808
  activeWorkflowId,
@@ -824,20 +810,20 @@ export async function createMixdogSessionRuntime({
824
810
  });
825
811
 
826
812
  const { contextStatus: computeContextStatus, invalidateContextStatusCache } = createContextStatus({
827
- getSession: () => session,
828
- getRoute: () => route,
829
- getCurrentCwd: () => currentCwd,
830
- getMode: () => mode,
813
+ getSession: () => rt.session,
814
+ getRoute: () => rt.route,
815
+ getCurrentCwd: () => rt.currentCwd,
816
+ getMode: () => rt.mode,
831
817
  });
832
818
  internalTools.setInternalToolsProvider({
833
819
  tools: [...standaloneTools, ...searchRuntimeTools.filter((tool) => tool?.public === false)],
834
820
  executor: async (name, args, callerCtx = {}) => {
835
- const callerCwd = callerCtx?.callerCwd || currentCwd;
821
+ const callerCwd = callerCtx?.callerCwd || rt.currentCwd;
836
822
  if (name === 'search' || name === 'web_fetch' || name === 'local_fetch' || name === 'image_fetch') {
837
823
  return dispatchSearchRuntimeTool(name, args, callerCtx, {
838
824
  getSearchModule,
839
- getCurrentCwd: () => currentCwd,
840
- getSession: () => session,
825
+ getCurrentCwd: () => rt.currentCwd,
826
+ getSession: () => rt.session,
841
827
  notifyFnForSession,
842
828
  runNativeWebSearch,
843
829
  });
@@ -848,7 +834,7 @@ export async function createMixdogSessionRuntime({
848
834
  return await memoryMod.handleToolCall(
849
835
  name,
850
836
  memoryToolArgsForCaller(args, callerCwd),
851
- callerCtx?.signal || session?.controller?.signal || null,
837
+ callerCtx?.signal || rt.session?.controller?.signal || null,
852
838
  );
853
839
  }
854
840
  if (name === 'code_graph') {
@@ -857,20 +843,20 @@ export async function createMixdogSessionRuntime({
857
843
  return await codeGraphMod.executeCodeGraphTool(name, args || {}, args?.cwd || callerCwd);
858
844
  }
859
845
  if (name === 'tool_search' || name === 'load_tool') {
860
- return renderToolSearch(args, activeToolSurface(), mode, { mcpStatus });
846
+ return renderToolSearch(args, activeToolSurface(), rt.mode, { mcpStatus });
861
847
  }
862
848
  if (name === 'explore') {
863
- const callerSessionId = callerCtx?.callerSessionId || session?.id || null;
849
+ const callerSessionId = callerCtx?.callerSessionId || rt.session?.id || null;
864
850
  return await runExplore(args || {}, {
865
851
  callerCwd: args?.cwd ? resolveCwdPath(args.cwd) : callerCwd,
866
852
  callerSessionId,
867
853
  routingSessionId: callerSessionId,
868
- clientHostPid: callerCtx?.clientHostPid || session?.clientHostPid || process.pid,
854
+ clientHostPid: callerCtx?.clientHostPid || rt.session?.clientHostPid || process.pid,
869
855
  notifyFn: notifyFnForSession(callerSessionId),
870
856
  // Cascade caller cancellation (ESC / owner abort) into the explore
871
857
  // fan-out so every child dispatch tears down immediately — same
872
858
  // signal the adjacent native search already forwards.
873
- signal: callerCtx?.signal || session?.controller?.signal,
859
+ signal: callerCtx?.signal || rt.session?.controller?.signal,
874
860
  });
875
861
  }
876
862
  if (name === 'cwd') {
@@ -880,17 +866,17 @@ export async function createMixdogSessionRuntime({
880
866
  } else if (action !== 'get') {
881
867
  throw new Error(`cwd: unknown action "${action}"`);
882
868
  }
883
- return JSON.stringify({ cwd: currentCwd, sessionId: session?.id || null }, null, 2);
869
+ return JSON.stringify({ cwd: rt.currentCwd, sessionId: rt.session?.id || null }, null, 2);
884
870
  }
885
871
  if (name === 'session_manage') {
886
872
  // Lead/owner sessions only: an agent worker resetting its own
887
873
  // transcript mid-task would corrupt the delegation contract, and it
888
874
  // must never reach the owner conversation either.
889
875
  const callerSessionId = callerCtx?.callerSessionId || null;
890
- if (callerSessionId && session?.id && callerSessionId !== session.id) {
876
+ if (callerSessionId && rt.session?.id && callerSessionId !== rt.session.id) {
891
877
  throw new Error('session_manage: only the lead session may reset the conversation');
892
878
  }
893
- if (!session?.id) throw new Error('session_manage: no active session');
879
+ if (!rt.session?.id) throw new Error('session_manage: no active session');
894
880
  const action = clean(args?.action).toLowerCase();
895
881
  if (action !== 'clear' && action !== 'compact_clear') {
896
882
  throw new Error(`session_manage: unknown action "${action}" (use clear | compact_clear)`);
@@ -900,7 +886,7 @@ export async function createMixdogSessionRuntime({
900
886
  // boundary the idle auto-clear uses).
901
887
  // Pin to the current session id so a resume/new-session between
902
888
  // scheduling and consumption can never clear the wrong conversation.
903
- pendingSessionReset = { action, sessionId: session.id };
889
+ rt.pendingSessionReset = { action, sessionId: rt.session.id };
904
890
  return action === 'clear'
905
891
  ? 'Session reset scheduled: full clear will run when this turn ends. All prior context will be gone.'
906
892
  : 'Session reset scheduled: the conversation will be summarized (compact) and cleared when this turn ends; key context carries forward in the summary.';
@@ -909,12 +895,12 @@ export async function createMixdogSessionRuntime({
909
895
  return skillToolContent(args?.name);
910
896
  }
911
897
  if (name === 'agent') {
912
- const callerSessionId = callerCtx?.callerSessionId || session?.id || null;
898
+ const callerSessionId = callerCtx?.callerSessionId || rt.session?.id || null;
913
899
  return await agentTool.execute(args, {
914
900
  callerCwd,
915
901
  invocationSource: 'model-tool',
916
902
  callerSessionId,
917
- clientHostPid: callerCtx?.clientHostPid || session?.clientHostPid || process.pid,
903
+ clientHostPid: callerCtx?.clientHostPid || rt.session?.clientHostPid || process.pid,
918
904
  signal: callerCtx?.signal,
919
905
  notifyFn: notifyFnForSession(callerSessionId),
920
906
  });
@@ -927,7 +913,7 @@ export async function createMixdogSessionRuntime({
927
913
  },
928
914
  });
929
915
  internalTools.markBootReady?.();
930
- try { lastProjectMcpKey = resolve(currentCwd) + '\u0000' + JSON.stringify(readProjectMcpServers(currentCwd)); } catch { lastProjectMcpKey = null; }
916
+ try { rt.lastProjectMcpKey = resolve(rt.currentCwd) + '\u0000' + JSON.stringify(readProjectMcpServers(rt.currentCwd)); } catch { rt.lastProjectMcpKey = null; }
931
917
  void connectConfiguredMcp()
932
918
  .then((status) => bootProfile('mcp:ready', {
933
919
  connected: Number(status?.connectedCount || 0),
@@ -976,13 +962,13 @@ export async function createMixdogSessionRuntime({
976
962
  displayConfig,
977
963
  ensureConfigForRouteProvider,
978
964
  } = createConfigLifecycle({
979
- getConfig: () => config,
980
- setConfig: (next) => { config = next; },
981
- getSearchRoute: () => searchRoute,
982
- setSearchRoute: (next) => { searchRoute = next; },
983
- getConfigHasSecrets: () => configHasSecrets,
984
- setConfigHasSecrets: (next) => { configHasSecrets = next; },
985
- getRoute: () => route,
965
+ getConfig: () => rt.config,
966
+ setConfig: (next) => { rt.config = next; },
967
+ getSearchRoute: () => rt.searchRoute,
968
+ setSearchRoute: (next) => { rt.searchRoute = next; },
969
+ getConfigHasSecrets: () => rt.configHasSecrets,
970
+ setConfigHasSecrets: (next) => { rt.configHasSecrets = next; },
971
+ getRoute: () => rt.route,
986
972
  cfgMod,
987
973
  sharedCfgMod,
988
974
  setBackend,
@@ -997,7 +983,7 @@ export async function createMixdogSessionRuntime({
997
983
  STANDALONE_DATA_DIR,
998
984
  });
999
985
 
1000
- async function ensureProvidersReady(providerConfig = config.providers || {}) {
986
+ async function ensureProvidersReady(providerConfig = rt.config.providers || {}) {
1001
987
  await awaitKeychainPrewarm();
1002
988
  const initKey = providerInitCacheKey(providerConfig);
1003
989
  const existing = providerInitPromises.get(initKey);
@@ -1017,16 +1003,16 @@ export async function createMixdogSessionRuntime({
1017
1003
  if (providerInitPromises.get(initKey) === providerInitPromise) providerInitPromises.delete(initKey);
1018
1004
  throw error;
1019
1005
  }
1020
- if (!startupProviderCatalogRefreshStarted && !closeRequested) {
1021
- startupProviderCatalogRefreshStarted = true;
1022
- startupProviderCatalogRefreshPending = true;
1006
+ if (!rt.startupProviderCatalogRefreshStarted && !rt.closeRequested) {
1007
+ rt.startupProviderCatalogRefreshStarted = true;
1008
+ rt.startupProviderCatalogRefreshPending = true;
1023
1009
  try {
1024
1010
  void Promise.resolve(reg.refreshProviderCatalogsOnStartup())
1025
1011
  .then(() => {
1026
1012
  // Fresh catalog rows invalidate model-derived caches, but the
1027
1013
  // already initialized provider registry remains valid.
1028
1014
  invalidateProviderCaches({ preserveProviderInit: true });
1029
- startupProviderCatalogRefreshPending = false;
1015
+ rt.startupProviderCatalogRefreshPending = false;
1030
1016
  // Secrets-aware: a no-secrets rewarm bumps the load sequence and
1031
1017
  // is never adopted, so it discarded the in-flight authoritative
1032
1018
  // load and left the picker cache empty — every later consumer then
@@ -1035,12 +1021,12 @@ export async function createMixdogSessionRuntime({
1035
1021
  bootProfile('provider-catalogs:refresh-ready');
1036
1022
  })
1037
1023
  .catch((error) => {
1038
- startupProviderCatalogRefreshPending = false;
1024
+ rt.startupProviderCatalogRefreshPending = false;
1039
1025
  bootProfile('provider-catalogs:refresh-failed', { error: error?.message || String(error) });
1040
1026
  });
1041
1027
  bootProfile('provider-catalogs:refresh-started');
1042
1028
  } catch (error) {
1043
- startupProviderCatalogRefreshPending = false;
1029
+ rt.startupProviderCatalogRefreshPending = false;
1044
1030
  bootProfile('provider-catalogs:refresh-failed', { error: error?.message || String(error) });
1045
1031
  }
1046
1032
  }
@@ -1051,11 +1037,11 @@ export async function createMixdogSessionRuntime({
1051
1037
  currentMainSearchModelMeta,
1052
1038
  runNativeWebSearch,
1053
1039
  } = createNativeSearch({
1054
- getRoute: () => route,
1055
- getSearchRoute: () => searchRoute,
1056
- setSearchRoute: (next) => { searchRoute = next; },
1057
- getConfig: () => config,
1058
- getSession: () => session,
1040
+ getRoute: () => rt.route,
1041
+ getSearchRoute: () => rt.searchRoute,
1042
+ setSearchRoute: (next) => { rt.searchRoute = next; },
1043
+ getConfig: () => rt.config,
1044
+ getSession: () => rt.session,
1059
1045
  getReg: () => reg,
1060
1046
  ensureFullConfig,
1061
1047
  awaitKeychainPrewarm,
@@ -1072,23 +1058,25 @@ export async function createMixdogSessionRuntime({
1072
1058
  // cachedProviderSetup(quick) may nudge scheduleProviderSetupWarmup on a cold
1073
1059
  // quick-cache fill. Thread it by reference so the scheduler is reachable once
1074
1060
  // it exists (a pre-scheduler quick fill simply skips the warmup nudge).
1075
- let scheduleProviderSetupWarmupRef = () => {};
1061
+ rt.scheduleProviderSetupWarmupRef = () => {};
1076
1062
  const {
1077
1063
  refreshStatuslineUsageSnapshot,
1078
1064
  cachedProviderSetup,
1079
1065
  hasProviderSetupCached,
1080
1066
  getUsageDashboard,
1067
+ consumeCodexRateLimitResetCredit,
1081
1068
  } = createProviderUsage({
1082
1069
  caches: providerUsageCaches,
1083
- getConfig: () => config,
1070
+ getConfig: () => rt.config,
1084
1071
  getReg: () => reg,
1085
1072
  displayConfig,
1086
1073
  providerSetup,
1087
1074
  createUsageDashboard,
1088
1075
  fetchOAuthUsageSnapshot,
1089
- isCloseRequested: () => closeRequested,
1076
+ consumeOpenAICodexResetCredit,
1077
+ isCloseRequested: () => rt.closeRequested,
1090
1078
  getProviderSetupWarmupTimer: () => warmupTimers.providerSetupWarmupTimer,
1091
- scheduleProviderSetupWarmup: (delayMs) => scheduleProviderSetupWarmupRef(delayMs),
1079
+ scheduleProviderSetupWarmup: (delayMs) => rt.scheduleProviderSetupWarmupRef(delayMs),
1092
1080
  });
1093
1081
 
1094
1082
  // Holder filled after createQuickModelRows resolves; provider-models and
@@ -1100,7 +1088,7 @@ export async function createMixdogSessionRuntime({
1100
1088
  // lookupModelMeta may fire scheduleProviderModelWarmup on a cache miss. Thread
1101
1089
  // it by reference so the scheduler is called once it exists (miss handling is
1102
1090
  // best-effort; a pre-scheduler miss simply skips the warmup nudge).
1103
- let scheduleProviderModelWarmupRef = () => {};
1091
+ rt.scheduleProviderModelWarmupRef = () => {};
1104
1092
  const {
1105
1093
  modelMetaKey,
1106
1094
  lookupModelMeta,
@@ -1113,8 +1101,8 @@ export async function createMixdogSessionRuntime({
1113
1101
  } = createProviderModels({
1114
1102
  caches: providerModelCaches,
1115
1103
  modelMetaByRoute,
1116
- getRoute: () => route,
1117
- getConfig: () => config,
1104
+ getRoute: () => rt.route,
1105
+ getConfig: () => rt.config,
1118
1106
  getReg: () => reg,
1119
1107
  searchCapableFor,
1120
1108
  sortProviderModelsRaw,
@@ -1125,7 +1113,7 @@ export async function createMixdogSessionRuntime({
1125
1113
  awaitKeychainPrewarm,
1126
1114
  ensureProvidersReady,
1127
1115
  bootProfile,
1128
- scheduleProviderModelWarmup: () => scheduleProviderModelWarmupRef(),
1116
+ scheduleProviderModelWarmup: () => rt.scheduleProviderModelWarmupRef(),
1129
1117
  quickHelpers: providerModelQuickHelpers,
1130
1118
  });
1131
1119
 
@@ -1136,8 +1124,8 @@ export async function createMixdogSessionRuntime({
1136
1124
  searchModelsFromRows,
1137
1125
  searchRowsWithDefault,
1138
1126
  } = createQuickModelRows({
1139
- getRoute: () => route,
1140
- getSearchRoute: () => searchRoute,
1127
+ getRoute: () => rt.route,
1128
+ getSearchRoute: () => rt.searchRoute,
1141
1129
  displayConfig,
1142
1130
  providerModelCacheRow,
1143
1131
  providerModelsFromCacheRows,
@@ -1158,289 +1146,71 @@ export async function createMixdogSessionRuntime({
1158
1146
  searchRowsWithDefault,
1159
1147
  });
1160
1148
 
1161
- async function resolveMissingRouteModelForFirstTurn() {
1162
- if (routeHasModel()) return route;
1163
- const models = await collectProviderModels();
1164
- const picked = models[0] || null;
1165
- if (!picked) {
1166
- throw new Error('No provider models available. Open /providers to sign in, then /model to choose a model.');
1167
- }
1168
- route = {
1169
- ...route,
1170
- provider: picked.provider,
1171
- model: picked.id,
1172
- preset: null,
1173
- };
1174
- return route;
1175
- }
1176
-
1177
- async function refreshRouteEffort(modelMetaOverride = null) {
1178
- await ensureProvidersReady(ensureProviderEnabled(config, route.provider));
1179
- const modelMeta = modelMetaOverride || await lookupModelMeta(route.provider, route.model);
1180
- const requested = hasOwn(route, 'effort') ? route.effort : (route.preset?.effort || null);
1181
- const effectiveEffort = coerceEffortFor(route.provider, modelMeta, requested);
1182
- const fastCapable = fastCapableFor(route.provider, modelMeta);
1183
- // Carry the catalog display name onto the route so the statusline shows a
1184
- // human label (e.g. "Claude Fable 5") for preset-less direct models instead
1185
- // of the raw id. `name` is only trusted when it differs from the raw model
1186
- // id (some providers echo the id as `name`), so it can't clobber a better
1187
- // already-resolved label. Falls back to existing route.modelDisplay, then unset.
1188
- const metaName = clean(modelMeta?.name);
1189
- const modelDisplay = clean(modelMeta?.display) || clean(modelMeta?.displayName)
1190
- || (metaName && metaName !== clean(route.model) ? metaName : '')
1191
- || clean(route.modelDisplay);
1192
- route = {
1193
- ...route,
1194
- fast: fastCapable ? route.fast === true : false,
1195
- fastCapable,
1196
- effectiveEffort,
1197
- effortOptions: effortItemsFor(route.provider, modelMeta, effectiveEffort),
1198
- ...(modelDisplay ? { modelDisplay } : {}),
1199
- };
1200
- return route;
1201
- }
1202
-
1203
- function routeHasModel() {
1204
- return !!clean(route?.model);
1205
- }
1206
-
1207
- function requireModelRoute() {
1208
- if (routeHasModel()) return;
1209
- throw new Error('No model configured. Open /providers to sign in, then /model to choose a model.');
1210
- }
1211
-
1212
- async function recreateCurrentSessionIfReady() {
1213
- if (!routeHasModel()) {
1214
- session = null;
1215
- return null;
1216
- }
1217
- return await createCurrentSession();
1218
- }
1219
-
1220
- async function createCurrentSession(reason = 'demand') {
1221
- if (sessionCreatePromise) return await sessionCreatePromise;
1222
- if (session?.id && !sessionNeedsCwdRefresh) {
1223
- const liveSession = mgr.getSession(session.id);
1224
- if (liveSession && liveSession.closed !== true && liveSession.status !== 'closed') {
1225
- session = liveSession;
1226
- return session;
1227
- }
1228
- session = null;
1229
- }
1230
-
1231
- const startedAt = performance.now();
1232
- bootProfile('session:create:start', { mode, reason });
1233
- const promise = (async () => {
1234
- // Demand-only: this starts only after the user submits (unless an
1235
- // explicitly enabled prewarm caller asks for a session). Core-memory
1236
- // startup does not depend on keychain/provider readiness, so overlap the
1237
- // two cold paths instead of paying their bounded waits serially.
1238
- const coreMemoryContextPromise = loadCoreMemoryContext();
1239
- await awaitKeychainPrewarm();
1240
- ensureConfigForRouteProvider();
1241
- await resolveMissingRouteModelForFirstTurn();
1242
- requireModelRoute();
1243
- bootProfile('session:create:route-ready', { ms: (performance.now() - startedAt).toFixed(1) });
1244
- // Route effort waits on provider readiness while the already-started
1245
- // memory load continues independently.
1246
- const [, coreMemoryContext] = await Promise.all([
1247
- refreshRouteEffort(),
1248
- coreMemoryContextPromise,
1249
- ]);
1250
- bootProfile('session:create:effort-ready', { ms: (performance.now() - startedAt).toFixed(1) });
1251
- const providerImpl = reg.getProvider(route.provider);
1252
- if (!providerImpl) {
1253
- throw new Error(`Provider "${route.provider}" is not configured.`);
1254
- }
1255
- bootProfile('session:create:provider-ready', { ms: (performance.now() - startedAt).toFixed(1) });
1256
- if (closeRequested) throw new Error('runtime is closing');
1257
- const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
1258
- // Load the active WORKFLOW.md pack once for both summary + context block.
1259
- const { summary: workflow, context: workflowContext } = activeWorkflowContext(config, dataDir);
1260
- const sessionOpts = {
1261
- provider: route.provider,
1262
- model: route.model,
1263
- preset: route.preset || undefined,
1264
- tools: toolSpecForMode(mode),
1265
- owner: 'cli',
1266
- agent: 'lead',
1267
- lane: 'cli',
1268
- sourceType: 'lead',
1269
- sourceName: 'main',
1270
- clientHostPid: process.pid,
1271
- disallowedTools: LEAD_DISALLOWED_TOOLS,
1272
- cwd: currentCwd,
1273
- ...(desktopSession && typeof desktopSession === 'object' ? { desktopSession } : {}),
1274
- coreMemoryContext,
1275
- workflow,
1276
- workflowContext,
1277
- fast: route.fast === true,
1278
- compaction: config.compaction && typeof config.compaction === 'object'
1279
- ? normalizeCompactionConfig(config.compaction, { memoryEnabled: memoryEnabled() })
1280
- : undefined,
1281
- };
1282
- if (hasOwn(route, 'effort') || route.effectiveEffort) {
1283
- sessionOpts.effort = route.effectiveEffort || null;
1284
- }
1285
- session = mgr.createSession(sessionOpts);
1286
- sessionNeedsCwdRefresh = false;
1287
- attachSessionHooks(session, { hooks, hookCommonPayload, getCwd: () => currentCwd });
1288
- // Every-create MCP fold (NO blocking): seed the INITIAL provider-visible
1289
- // surface (and native BP1 manifest) from MCP servers connected at create
1290
- // time. There is no await — a boot connect still mid-handshake is caught on
1291
- // the first user turn by refreshInitialDeferredMcpSurface (session-turn-api),
1292
- // which re-folds the live registry into the first-turn surface before the
1293
- // prompt renders. This fold keeps recreate paths (cwd change with MCP
1294
- // already connected) seeding their manifest instead of re-announcing late.
1295
- let connectedMcpTools = [];
1296
- try { connectedMcpTools = mcpClient.getMcpTools?.() || []; }
1297
- catch { connectedMcpTools = []; }
1298
- applyDeferredToolSurface(
1299
- session,
1300
- deferredSurfaceModeForLead(mode),
1301
- connectedMcpTools.length ? [...modelStandaloneTools(), ...connectedMcpTools] : modelStandaloneTools(),
1302
- { provider: route.provider },
1303
- );
1304
- // Session-local one-shot: mark this FRESH session eligible for the
1305
- // first-turn deferred-surface refresh (session-turn-api). A resumed
1306
- // session (prior transcript) is NEVER marked, so its already-baked BP1 is
1307
- // never rebuilt or re-announced — the gate is per-session, not the
1308
- // process-wide firstTurnCompleted.
1309
- session.deferredInitialRefreshPending = !/resume/i.test(String(reason || ''));
1310
- applyPreSessionToolSelection();
1311
- writeStatuslineRoute(statusRoutes, session, route);
1312
- hooks.emit('session:create', { sessionId: session.id, provider: route.provider, model: route.model, toolMode: mode, cwd: currentCwd });
1313
- // SessionStart: bridge to the standard project hook bus. Best-effort;
1314
- // a hook error must never break session creation. additionalContext is
1315
- // injected before the first user turn as a system-reminder context pair.
1316
- try {
1317
- const startSource = /resume/i.test(String(reason || ''))
1318
- ? 'resume'
1319
- : (/clear/i.test(String(reason || '')) ? 'clear' : 'startup');
1320
- const startDispatch = await hooks.dispatch('SessionStart', hookCommonPayload({ session_id: session.id, source: startSource, model: route.model }));
1321
- const startContext = Array.isArray(startDispatch?.additionalContext)
1322
- ? startDispatch.additionalContext.join('\n\n')
1323
- : String(startDispatch?.additionalContext || '');
1324
- if (startContext.trim()) {
1325
- session.messages.push({ role: 'user', content: `<system-reminder>\n# SessionStart Hook Context\n${startContext.trim()}\n</system-reminder>` });
1326
- session.messages.push({ role: 'assistant', content: '.' });
1327
- session.updatedAt = Date.now();
1328
- }
1329
- } catch { /* best-effort: never break session create */ }
1330
- bootProfile('session:create:ready', {
1331
- ms: (performance.now() - startedAt).toFixed(1),
1332
- reason,
1333
- tools: Array.isArray(session.tools) ? session.tools.length : 0,
1334
- catalog: Array.isArray(session.deferredToolCatalog) ? session.deferredToolCatalog.length : 0,
1335
- });
1336
- // A rebind push may have been deferred (e.g. 'acquired' landed before this
1337
- // session existed). The writer is now bindable — flush it exactly once.
1338
- flushPendingTranscriptRebind();
1339
- return session;
1340
- })();
1341
-
1342
- sessionCreatePromise = promise;
1343
- try {
1344
- return await promise;
1345
- } finally {
1346
- if (sessionCreatePromise === promise) sessionCreatePromise = null;
1347
- }
1348
- }
1349
-
1149
+ // Route resolution + createCurrentSession: session-lifecycle.mjs.
1350
1150
  const {
1151
+ resolveMissingRouteModelForFirstTurn,
1351
1152
  scheduleProviderWarmup,
1352
1153
  scheduleProviderSetupWarmup,
1353
1154
  scheduleProviderModelWarmup,
1354
1155
  scheduleModelCatalogWarmup,
1355
1156
  scheduleStatuslineUsageWarmup,
1356
1157
  scheduleStatuslineUsageRefresh,
1357
- } = createWarmupSchedulers({
1358
- timers: warmupTimers,
1359
- bootProfile,
1360
- getRoute: () => route,
1361
- getConfig: () => config,
1362
- isCloseRequested: () => closeRequested,
1363
- getActiveTurnCount: () => activeTurnCount,
1364
- getSessionCreatePromise: () => sessionCreatePromise,
1365
- getProviderModelsCache: () => providerModelCaches.providerModelsCache,
1366
- getProviderModelsPromise: () => providerModelCaches.providerModelsPromise,
1367
- reloadFullConfig,
1368
- ensureConfigForRouteProvider,
1369
- awaitKeychainPrewarm,
1370
- ensureProvidersReady,
1371
- ensureProviderEnabled,
1372
- refreshStatuslineUsageSnapshot,
1373
- warmProviderModelCache,
1374
- cachedProviderSetup,
1375
- warmCatalogsInBackground,
1376
- isFirstTurnCompleted: () => firstTurnCompleted,
1377
- isCatalogRefreshPending: () => startupProviderCatalogRefreshPending,
1378
- envFlag,
1379
- delays: {
1380
- providerWarmupDelayMs,
1381
- providerSetupWarmupDelayMs,
1382
- providerModelWarmupDelayMs,
1383
- modelCatalogWarmupDelayMs,
1384
- statuslineUsageWarmupDelayMs,
1385
- statuslineUsageRefreshDelayMs,
1386
- backgroundBusyRetryMs,
1387
- },
1388
- flags: {
1389
- providerWarmupEnabled,
1390
- modelPrefetchEnabled,
1391
- modelCatalogWarmupEnabled,
1392
- },
1393
- });
1394
- scheduleProviderModelWarmupRef = scheduleProviderModelWarmup;
1395
- scheduleProviderSetupWarmupRef = scheduleProviderSetupWarmup;
1396
-
1397
- const {
1398
1158
  scheduleCodeGraphPrewarm,
1399
1159
  scheduleLeadSessionPrewarm,
1160
+ scheduleToolRuntimeWarmup,
1400
1161
  invokeChannelStart,
1401
1162
  scheduleChannelStart,
1402
- } = createPrewarmSchedulers({
1403
- timers: prewarmTimers,
1404
- bootProfile,
1405
- getCurrentCwd: () => currentCwd,
1406
- isCloseRequested: () => closeRequested,
1407
- getActiveTurnCount: () => activeTurnCount,
1408
- getSessionCreatePromise: () => sessionCreatePromise,
1409
- getSession: () => session,
1410
- isRemoteEnabled: () => remoteEnabled,
1411
- channelsEnabled,
1412
- hasActiveAutomation,
1413
- getCodeGraphModule,
1163
+ refreshRouteEffort,
1164
+ routeHasModel,
1165
+ requireModelRoute,
1166
+ recreateCurrentSessionIfReady,
1414
1167
  createCurrentSession,
1415
- channels,
1416
- envFlag,
1417
- delays: {
1418
- codeGraphPrewarmDelayMs,
1419
- channelStartDelayMs,
1420
- sessionPrewarmDelayMs,
1421
- backgroundBusyRetryMs,
1422
- },
1423
- flags: {
1424
- codeGraphPrewarmEnabled,
1425
- sessionPrewarmEnabled,
1426
- },
1427
- state: prewarmState,
1428
- });
1429
-
1430
- // Remote transcript binding + worker rebind pushes live in
1431
- // remote-transcript.mjs; the facade injects the mutable session/cwd/remote
1432
- // state it needs.
1433
- const remoteTranscript = createRemoteTranscript({
1434
- getSession: () => session,
1435
- getCwd: () => currentCwd,
1436
- isRemoteEnabled: () => remoteEnabled,
1437
- getRemoteSessionId: () => remoteSessionId,
1438
- setRemoteSessionId: (next) => { remoteSessionId = next; },
1439
- isCloseRequested: () => closeRequested,
1168
+ remoteTranscript,
1169
+ } = createSessionLifecycle({
1170
+ rt,
1171
+ collectProviderModels,
1172
+ ensureProvidersReady,
1173
+ lookupModelMeta,
1174
+ mgr,
1175
+ loadCoreMemoryContext,
1176
+ awaitKeychainPrewarm,
1177
+ ensureConfigForRouteProvider,
1178
+ reg,
1179
+ cfgMod,
1180
+ activeWorkflowContext,
1181
+ memoryEnabled,
1182
+ hooks,
1183
+ hookCommonPayload,
1184
+ mcpClient,
1185
+ modelStandaloneTools,
1186
+ applyPreSessionToolSelection,
1187
+ statusRoutes,
1188
+ warmupTimers,
1189
+ providerModelCaches,
1190
+ reloadFullConfig,
1191
+ refreshStatuslineUsageSnapshot,
1192
+ warmProviderModelCache,
1193
+ cachedProviderSetup,
1194
+ providerWarmupDelayMs,
1195
+ providerSetupWarmupDelayMs,
1196
+ providerModelWarmupDelayMs,
1197
+ modelCatalogWarmupDelayMs,
1198
+ statuslineUsageWarmupDelayMs,
1199
+ statuslineUsageRefreshDelayMs,
1200
+ backgroundBusyRetryMs,
1201
+ providerWarmupEnabled,
1202
+ modelPrefetchEnabled,
1203
+ modelCatalogWarmupEnabled,
1204
+ prewarmTimers,
1440
1205
  channelsEnabled,
1206
+ getCodeGraphModule,
1441
1207
  channels,
1442
- mgr,
1443
- bootProfile,
1208
+ codeGraphPrewarmDelayMs,
1209
+ channelStartDelayMs,
1210
+ sessionPrewarmDelayMs,
1211
+ codeGraphPrewarmEnabled,
1212
+ sessionPrewarmEnabled,
1213
+ prewarmState,
1444
1214
  });
1445
1215
  const ensureRemoteTranscriptWriter = () => remoteTranscript.ensureRemoteTranscriptWriter();
1446
1216
  const pushTranscriptRebind = () => remoteTranscript.pushTranscriptRebind();
@@ -1449,14 +1219,12 @@ export async function createMixdogSessionRuntime({
1449
1219
  // Remote (channel relay) claim/release/stop live in remote-control.mjs; the
1450
1220
  // facade injects the mutable session + remote state they mutate.
1451
1221
  const remoteControl = createRemoteControl({
1452
- getSession: () => session,
1453
- isRemoteEnabled: () => remoteEnabled,
1454
- setRemoteEnabled: (next) => { remoteEnabled = next; },
1455
- getRemoteSessionId: () => remoteSessionId,
1456
- setRemoteSessionId: (next) => { remoteSessionId = next; },
1457
- isRemoteClaimPending: () => remoteClaimPending,
1458
- setRemoteClaimPending: (next) => { remoteClaimPending = next; },
1459
- isCloseRequested: () => closeRequested,
1222
+ getSession: () => rt.session,
1223
+ isRemoteEnabled: () => rt.remoteEnabled,
1224
+ setRemoteEnabled: (next) => { rt.remoteEnabled = next; },
1225
+ getRemoteSessionId: () => rt.remoteSessionId,
1226
+ setRemoteSessionId: (next) => { rt.remoteSessionId = next; },
1227
+ isCloseRequested: () => rt.closeRequested,
1460
1228
  clearChannelStartTimer: () => {
1461
1229
  if (prewarmTimers.channelStartTimer) {
1462
1230
  clearTimeout(prewarmTimers.channelStartTimer);
@@ -1482,7 +1250,7 @@ export async function createMixdogSessionRuntime({
1482
1250
  const stopRemote = (reason) => remoteControl.stopRemote(reason);
1483
1251
 
1484
1252
  function isRemoteEnabled() {
1485
- return remoteEnabled;
1253
+ return rt.remoteEnabled;
1486
1254
  }
1487
1255
 
1488
1256
  function withTeardownDeadline(promise, ms, fallback = false) {
@@ -1504,6 +1272,7 @@ export async function createMixdogSessionRuntime({
1504
1272
  codeGraphPrewarm: codeGraphPrewarmEnabled,
1505
1273
  });
1506
1274
  scheduleLeadSessionPrewarm();
1275
+ scheduleToolRuntimeWarmup();
1507
1276
  // Lazy mode (default): skip the startup prewarm entirely; the first turn
1508
1277
  // triggers it instead (see ask()). Eager mode keeps the old startup schedule.
1509
1278
  if (!codeGraphPrewarmLazy) {
@@ -1523,32 +1292,7 @@ export async function createMixdogSessionRuntime({
1523
1292
  // Channels are opt-in: only boot the worker when this session started in (or
1524
1293
  // was toggled into) remote mode. Non-remote sessions never contend for the
1525
1294
  // channel; see startRemote()/stopRemote() and the `/remote` toggle.
1526
- // `remote.autoStart` in mixdog-config.json makes terminal sessions attempt a
1527
- // remote claim at boot. Desktop sessions are explicit-only: opening the app
1528
- // must never claim or spawn the channel bridge until its Remote button is
1529
- // pressed, while the shared owner-state watcher still reflects a terminal
1530
- // that already owns it.
1531
- // The flag lives in the TOP-LEVEL `remote` section of mixdog-config.json
1532
- // (sibling of agent/ui/channels), not inside the agent section that
1533
- // cfgMod.loadConfig returns — read it via the shared whole-file reader.
1534
- // `config?.remote?.autoStart` is kept for back-compat with agent-section
1535
- // placement.
1536
- // `remote` (from `mixdog --remote`) is EXPLICIT force-takeover. Config
1537
- // `remote.autoStart` is AUTO: it must claim the seat ONLY if no live owner
1538
- // exists (claim-if-vacant) and back off silently otherwise — so it does NOT
1539
- // set remoteEnabled up front (that would assert remote before the claim is
1540
- // known to have won). Track it as a separate deferred auto request; the
1541
- // worker's acquire notification flips remoteEnabled if/when it wins the seat.
1542
- let remoteAutoStartRequested = false;
1543
- if (!remoteEnabled && !desktopSession) {
1544
- try {
1545
- if (config?.remote?.autoStart === true
1546
- || sharedCfgMod?.readSection?.('remote')?.autoStart === true) {
1547
- remoteAutoStartRequested = true;
1548
- }
1549
- } catch { /* unreadable config never blocks boot */ }
1550
- }
1551
- // Boot-time remote start (autoStart or --remote) is DEFERRED past the TUI's
1295
+ // A manual `mixdog --remote` request is deferred past the TUI's
1552
1296
  // first frame. startRemote() front-loads heavy work — memory daemon fork
1553
1297
  // (PG + forced ONNX embed warmup in the child), eager session create, and
1554
1298
  // the channel-worker fork — and running it inline here interleaves that
@@ -1557,29 +1301,25 @@ export async function createMixdogSessionRuntime({
1557
1301
  // early /remote (startRemote clears it), stopRemote(), and close() all
1558
1302
  // cancel it through the existing clearTimeout paths. Runtime /remote calls
1559
1303
  // still start immediately (user-initiated, UI already painted).
1560
- if (remoteEnabled || remoteAutoStartRequested) {
1561
- const remoteStartIntent = remoteEnabled ? 'explicit' : 'auto';
1562
- bootProfile('channels:autostart-deferred', { delayMs: remoteAutoStartDelayMs, intent: remoteStartIntent });
1304
+ if (rt.remoteEnabled) {
1305
+ bootProfile('channels:manual-start-deferred', { delayMs: remoteAutoStartDelayMs });
1563
1306
  prewarmTimers.channelStartTimer = setTimeout(() => {
1564
1307
  prewarmTimers.channelStartTimer = null;
1565
- if (closeRequested) return;
1566
- // Explicit: a /remote-off before the timer clears remoteEnabled — skip.
1567
- // Auto: always attempt (claim-if-vacant is safe when a live owner exists).
1568
- if (remoteStartIntent === 'explicit' && !remoteEnabled) return;
1569
- startRemote({ intent: remoteStartIntent });
1308
+ if (rt.closeRequested || !rt.remoteEnabled) return;
1309
+ startRemote();
1570
1310
  }, remoteAutoStartDelayMs);
1571
1311
  prewarmTimers.channelStartTimer.unref?.();
1572
1312
  } else {
1573
1313
  // Automation decoupling (user decision): enabled schedules/webhooks boot
1574
1314
  // the worker on their own — no remote flag, no remote.autoStart, and no
1575
- // messaging backend. A later explicit/auto Remote claim promotes the same
1315
+ // messaging backend. A later explicit Remote ON promotes the same
1576
1316
  // daemon without restarting schedules or webhooks.
1577
1317
  prewarmTimers.channelStartTimer = setTimeout(() => {
1578
1318
  prewarmTimers.channelStartTimer = null;
1579
- if (closeRequested || remoteEnabled) return;
1319
+ if (rt.closeRequested || rt.remoteEnabled) return;
1580
1320
  void hasActiveAutomation()
1581
1321
  .then((active) => {
1582
- if (!active || closeRequested || remoteEnabled) return;
1322
+ if (!active || rt.closeRequested || rt.remoteEnabled) return;
1583
1323
  bootProfile('channels:automation-autostart');
1584
1324
  void invokeChannelStart();
1585
1325
  })
@@ -1593,10 +1333,10 @@ export async function createMixdogSessionRuntime({
1593
1333
  // token save/forget, setBackend). Extracted to session-runtime/settings-api.mjs
1594
1334
  // and SPREAD into the API object below so the external surface is unchanged.
1595
1335
  const settingsApi = createSettingsApi({
1596
- getConfig: () => config,
1597
- getRoute: () => route,
1598
- getSession: () => session,
1599
- getRemoteEnabled: () => remoteEnabled,
1336
+ getConfig: () => rt.config,
1337
+ getRoute: () => rt.route,
1338
+ getSession: () => rt.session,
1339
+ getRemoteEnabled: () => rt.remoteEnabled,
1600
1340
  adoptConfig,
1601
1341
  saveConfigAndAdopt,
1602
1342
  scheduleBackendSave,
@@ -1655,35 +1395,36 @@ export async function createMixdogSessionRuntime({
1655
1395
  });
1656
1396
  const providerAuthApi = createProviderAuthApi({
1657
1397
  cfgMod,
1658
- getConfig: () => config,
1398
+ getConfig: () => rt.config,
1659
1399
  saveConfigAndAdopt,
1660
1400
  displayConfig,
1661
1401
  reloadFullConfig,
1662
1402
  awaitKeychainPrewarm,
1663
- isKeychainPrewarmReady: () => keychainPrewarmWaitDone,
1403
+ isKeychainPrewarmReady: () => rt.keychainPrewarmWaitDone,
1664
1404
  hasProviderSetupCached,
1665
1405
  invalidateProviderCaches,
1666
1406
  warmProviderModelCache,
1667
- refreshProviderCatalogs: () => ensureProvidersReady(config.providers || {}).then(() => reg.refreshCatalogs()),
1407
+ refreshProviderCatalogs: () => ensureProvidersReady(rt.config.providers || {}).then(() => reg.refreshCatalogs()),
1668
1408
  cachedProviderSetup,
1669
1409
  getUsageDashboard,
1410
+ consumeCodexRateLimitResetCredit,
1670
1411
  collectProviderModels,
1671
1412
  });
1672
1413
  const mediaApi = createMediaApi();
1673
1414
  const lifecycleApi = createLifecycleApi({
1674
- getSession: () => session,
1675
- setSession: (v) => { session = v; },
1676
- getRoute: () => route,
1677
- setRoute: (v) => { route = v; },
1678
- getConfig: () => config,
1679
- getMode: () => mode,
1680
- getCurrentCwd: () => currentCwd,
1681
- getDesktopSession: () => desktopSession,
1682
- setDesktopSession: (v) => { desktopSession = v; },
1683
- setCloseRequested: (v) => { closeRequested = v; },
1684
- getMemoryModPromise: () => memoryModPromise,
1685
- setMemoryModPromise: (v) => { memoryModPromise = v; },
1686
- setSessionNeedsCwdRefresh: (v) => { sessionNeedsCwdRefresh = v; },
1415
+ getSession: () => rt.session,
1416
+ setSession: (v) => { rt.session = v; },
1417
+ getRoute: () => rt.route,
1418
+ setRoute: (v) => { rt.route = v; },
1419
+ getConfig: () => rt.config,
1420
+ getMode: () => rt.mode,
1421
+ getCurrentCwd: () => rt.currentCwd,
1422
+ getDesktopSession: () => rt.desktopSession,
1423
+ setDesktopSession: (v) => { rt.desktopSession = v; },
1424
+ setCloseRequested: (v) => { rt.closeRequested = v; },
1425
+ getMemoryModPromise: () => rt.memoryModPromise,
1426
+ setMemoryModPromise: (v) => { rt.memoryModPromise = v; },
1427
+ setSessionNeedsCwdRefresh: (v) => { rt.sessionNeedsCwdRefresh = v; },
1687
1428
  hooks,
1688
1429
  hookCommonPayload,
1689
1430
  mgr,
@@ -1707,15 +1448,17 @@ export async function createMixdogSessionRuntime({
1707
1448
  applyResolvedCwd,
1708
1449
  resolveRoute,
1709
1450
  applyDeferredToolSurface,
1451
+ beginRoutePreparation: (task) => routePreparation.start(task),
1452
+ clearRoutePreparation: () => routePreparation.clear(),
1710
1453
  // Live getter: cwd-refresh session rebuilds must re-evaluate the
1711
1454
  // workflow's agent-tool gate, not reuse the boot-time array.
1712
1455
  getStandaloneTools: modelStandaloneTools,
1713
- desktopSession,
1456
+ desktopSession: rt.desktopSession,
1714
1457
  });
1715
1458
  const resourceApi = createResourceApi({
1716
- getConfig: () => config,
1717
- getSession: () => session,
1718
- getCurrentCwd: () => currentCwd,
1459
+ getConfig: () => rt.config,
1460
+ getSession: () => rt.session,
1461
+ getCurrentCwd: () => rt.currentCwd,
1719
1462
  cfgMod,
1720
1463
  mgr,
1721
1464
  hooks,
@@ -1733,17 +1476,17 @@ export async function createMixdogSessionRuntime({
1733
1476
  getMemoryModule,
1734
1477
  reloadFullConfig,
1735
1478
  awaitKeychainPrewarm,
1736
- getActiveTurnCount: () => activeTurnCount,
1479
+ getActiveTurnCount: () => rt.activeTurnCount,
1737
1480
  });
1738
1481
  const modelRouteApi = createModelRouteApi({
1739
- getConfig: () => config,
1740
- getRoute: () => route,
1741
- setRouteState: (v) => { route = v; },
1742
- getSession: () => session,
1743
- setSession: (v) => { session = v; },
1744
- getConfigHasSecrets: () => configHasSecrets,
1745
- getSearchRouteState: () => searchRoute,
1746
- setSearchRouteState: (v) => { searchRoute = v; },
1482
+ getConfig: () => rt.config,
1483
+ getRoute: () => rt.route,
1484
+ setRouteState: (v) => { rt.route = v; },
1485
+ getSession: () => rt.session,
1486
+ setSession: (v) => { rt.session = v; },
1487
+ getConfigHasSecrets: () => rt.configHasSecrets,
1488
+ getSearchRouteState: () => rt.searchRoute,
1489
+ setSearchRouteState: (v) => { rt.searchRoute = v; },
1747
1490
  cfgMod,
1748
1491
  reg,
1749
1492
  mgr,
@@ -1768,12 +1511,12 @@ export async function createMixdogSessionRuntime({
1768
1511
  collectSearchProviderModels,
1769
1512
  });
1770
1513
  const workflowAgentsApi = createWorkflowAgentsApi({
1771
- getConfig: () => config,
1772
- getRoute: () => route,
1773
- setRouteState: (v) => { route = v; },
1774
- getSession: () => session,
1775
- setSession: (v) => { session = v; },
1776
- getConfigHasSecrets: () => configHasSecrets,
1514
+ getConfig: () => rt.config,
1515
+ getRoute: () => rt.route,
1516
+ setRouteState: (v) => { rt.route = v; },
1517
+ getSession: () => rt.session,
1518
+ setSession: (v) => { rt.session = v; },
1519
+ getConfigHasSecrets: () => rt.configHasSecrets,
1777
1520
  cfgMod,
1778
1521
  reg,
1779
1522
  mgr,
@@ -1799,26 +1542,26 @@ export async function createMixdogSessionRuntime({
1799
1542
  invalidateContextStatusCache,
1800
1543
  });
1801
1544
  const sessionTurnApi = createSessionTurnApi({
1802
- getSession: () => session,
1803
- setSession: (v) => { session = v; },
1804
- getCurrentCwd: () => currentCwd,
1805
- getMode: () => mode,
1806
- setMode: (v) => { mode = v; },
1807
- getActiveTurnCount: () => activeTurnCount,
1808
- setActiveTurnCount: (v) => { activeTurnCount = v; },
1809
- isFirstTurnCompleted: () => firstTurnCompleted,
1810
- setFirstTurnCompleted: (v) => { firstTurnCompleted = v; },
1811
- getCodeGraphFirstTurnPrewarmDone: () => codeGraphFirstTurnPrewarmDone,
1812
- setCodeGraphFirstTurnPrewarmDone: (v) => { codeGraphFirstTurnPrewarmDone = v; },
1545
+ getSession: () => rt.session,
1546
+ setSession: (v) => { rt.session = v; },
1547
+ getCurrentCwd: () => rt.currentCwd,
1548
+ getMode: () => rt.mode,
1549
+ setMode: (v) => { rt.mode = v; },
1550
+ getActiveTurnCount: () => rt.activeTurnCount,
1551
+ setActiveTurnCount: (v) => { rt.activeTurnCount = v; },
1552
+ isFirstTurnCompleted: () => rt.firstTurnCompleted,
1553
+ setFirstTurnCompleted: (v) => { rt.firstTurnCompleted = v; },
1554
+ getCodeGraphFirstTurnPrewarmDone: () => rt.codeGraphFirstTurnPrewarmDone,
1555
+ setCodeGraphFirstTurnPrewarmDone: (v) => { rt.codeGraphFirstTurnPrewarmDone = v; },
1813
1556
  codeGraphPrewarmLazy,
1814
- getRemoteEnabled: () => remoteEnabled,
1815
- getCloseRequested: () => closeRequested,
1816
- getPendingSessionReset: () => pendingSessionReset,
1817
- setPendingSessionReset: (v) => { pendingSessionReset = v; },
1557
+ getRemoteEnabled: () => rt.remoteEnabled,
1558
+ getCloseRequested: () => rt.closeRequested,
1559
+ getPendingSessionReset: () => rt.pendingSessionReset,
1560
+ setPendingSessionReset: (v) => { rt.pendingSessionReset = v; },
1818
1561
  getTranscriptWriter: () => remoteTranscript.transcriptWriter,
1819
1562
  getTwKey: () => remoteTranscript.transcriptKey,
1820
- getLastAppendedAssistant: () => _lastAppendedAssistant,
1821
- setLastAppendedAssistant: (v) => { _lastAppendedAssistant = v; },
1563
+ getLastAppendedAssistant: () => rt._lastAppendedAssistant,
1564
+ setLastAppendedAssistant: (v) => { rt._lastAppendedAssistant = v; },
1822
1565
  scheduleCodeGraphPrewarm,
1823
1566
  refreshSessionForCwdIfNeeded,
1824
1567
  createCurrentSession,
@@ -1846,6 +1589,7 @@ export async function createMixdogSessionRuntime({
1846
1589
  notificationListeners,
1847
1590
  remoteStateListeners,
1848
1591
  awaitInitialMcpConnect,
1592
+ awaitRoutePreparation: () => routePreparation.wait(),
1849
1593
  });
1850
1594
 
1851
1595
  return {
@@ -1855,55 +1599,55 @@ export async function createMixdogSessionRuntime({
1855
1599
  ...mediaApi,
1856
1600
  // Turn-scoped attributed review: child apply_patch diffs for this Lead turn.
1857
1601
  // Lead patches are read from its transcript by the renderer.
1858
- getTurnReviewDiff: () => getTurnSnapshotReviewDiff(currentCwd, session?.id),
1602
+ getTurnReviewDiff: () => getTurnSnapshotReviewDiff(rt.currentCwd, rt.session?.id),
1859
1603
  get id() {
1860
- return session?.id || null;
1604
+ return rt.session?.id || null;
1861
1605
  },
1862
1606
  get provider() {
1863
- return route.provider;
1607
+ return rt.route.provider;
1864
1608
  },
1865
1609
  get model() {
1866
- return route.model;
1610
+ return rt.route.model;
1867
1611
  },
1868
1612
  get effort() {
1869
- return route.effectiveEffort || route.effort || route.preset?.effort || null;
1613
+ return rt.route.effectiveEffort || rt.route.effort || rt.route.preset?.effort || null;
1870
1614
  },
1871
1615
  get fast() {
1872
- return route.fast === true;
1616
+ return rt.route.fast === true;
1873
1617
  },
1874
1618
  get fastCapable() {
1875
- return route.fastCapable === true;
1619
+ return rt.route.fastCapable === true;
1876
1620
  },
1877
1621
  get effortOptions() {
1878
- return route.effortOptions || [];
1622
+ return rt.route.effortOptions || [];
1879
1623
  },
1880
1624
  get contextWindow() {
1881
- return session?.contextWindow || null;
1625
+ return rt.session?.contextWindow || null;
1882
1626
  },
1883
1627
  get rawContextWindow() {
1884
- return session?.rawContextWindow || session?.contextWindow || null;
1628
+ return rt.session?.rawContextWindow || rt.session?.contextWindow || null;
1885
1629
  },
1886
1630
  get effectiveContextWindowPercent() {
1887
- return session?.effectiveContextWindowPercent || null;
1631
+ return rt.session?.effectiveContextWindowPercent || null;
1888
1632
  },
1889
1633
  get toolMode() {
1890
- return mode;
1634
+ return rt.mode;
1891
1635
  },
1892
1636
  get autoClear() {
1893
1637
  return this.getAutoClear();
1894
1638
  },
1895
1639
  get systemShell() {
1896
- return normalizeSystemShellConfig(config.shell);
1640
+ return normalizeSystemShellConfig(rt.config.shell);
1897
1641
  },
1898
1642
  get searchRoute() {
1899
- searchRoute = normalizeSearchRouteConfig(config.searchRoute) || normalizeSearchRouteConfig(searchRoute);
1900
- return searchRoute;
1643
+ rt.searchRoute = normalizeSearchRouteConfig(rt.config.searchRoute) || normalizeSearchRouteConfig(rt.searchRoute);
1644
+ return rt.searchRoute;
1901
1645
  },
1902
1646
  get workflow() {
1903
1647
  const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
1904
- const active = activeWorkflowSummary(config, dataDir);
1905
- if (session?.workflow && typeof session.workflow === 'object') {
1906
- const current = workflowSummary(session.workflow);
1648
+ const active = activeWorkflowSummary(rt.config, dataDir);
1649
+ if (rt.session?.workflow && typeof rt.session.workflow === 'object') {
1650
+ const current = workflowSummary(rt.session.workflow);
1907
1651
  return current?.id && active?.id && current.id !== active.id
1908
1652
  ? { ...active, currentSession: current, appliedToCurrentSession: false }
1909
1653
  : active;
@@ -1914,10 +1658,10 @@ export async function createMixdogSessionRuntime({
1914
1658
  return getOutputStyleStatusCached().current;
1915
1659
  },
1916
1660
  get cwd() {
1917
- return currentCwd;
1661
+ return rt.currentCwd;
1918
1662
  },
1919
1663
  get session() {
1920
- return session;
1664
+ return rt.session;
1921
1665
  },
1922
1666
  contextStatus() {
1923
1667
  // Prefer the in-flight working transcript while a turn is running so the
@@ -1942,17 +1686,11 @@ export async function createMixdogSessionRuntime({
1942
1686
  },
1943
1687
  // Session-scoped remote: the owning session id (null when off/unassigned).
1944
1688
  getRemoteSessionId() {
1945
- return remoteSessionId;
1689
+ return rt.remoteSessionId;
1946
1690
  },
1947
- // Move the relay seat to the CURRENT session (last-wins within this
1948
- // engine): rebind the transcript writer + forwarder without restarting
1949
- // the worker. No-op when remote is off or no session exists.
1691
+ // Back-compat alias for an explicit manual ON in the current session.
1950
1692
  claimRemoteForCurrentSession() {
1951
- if (!remoteEnabled || !session?.id) return false;
1952
- remoteSessionId = session.id;
1953
- ensureRemoteTranscriptWriter();
1954
- pushTranscriptRebind();
1955
- return true;
1693
+ return startRemote();
1956
1694
  },
1957
1695
  // Subscribe to non-user-initiated remote flips (seat superseded). Returns
1958
1696
  // an unsubscribe function. TUI uses this to sync its Remote indicator and
@@ -1963,7 +1701,7 @@ export async function createMixdogSessionRuntime({
1963
1701
  return () => remoteStateListeners.delete(listener);
1964
1702
  },
1965
1703
  get clientHostPid() {
1966
- return session?.clientHostPid || process.pid;
1704
+ return rt.session?.clientHostPid || process.pid;
1967
1705
  },
1968
1706
  ...lifecycleApi,
1969
1707
  ...resourceApi,