mixdog 0.9.95 → 0.9.97

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 (282) hide show
  1. package/LICENSES/Apache-2.0.txt +201 -0
  2. package/LICENSES/MIT.txt +56 -0
  3. package/LICENSES/codex-NOTICE.txt +6 -0
  4. package/NOTICE.md +46 -9
  5. package/README.md +0 -17
  6. package/package.json +16 -12
  7. package/scripts/.tmp-coverage-report.mjs +96 -0
  8. package/scripts/build-tui.mjs +17 -1
  9. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  10. package/scripts/fixtures/patch-replay-corpus.json +98 -0
  11. package/src/defaults/mixdog-config.template.json +5 -8
  12. package/src/defaults/skills/setup/SKILL.md +1 -1
  13. package/src/lib/keychain-cjs.cjs +1 -1
  14. package/src/lib/mixdog-debug.cjs +2 -3
  15. package/src/rules/agent/30-explorer.md +7 -4
  16. package/src/rules/lead/01-general.md +1 -2
  17. package/src/rules/lead/lead-tool.md +0 -3
  18. package/src/rules/shared/01-tool.md +21 -29
  19. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
  20. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
  23. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  24. package/src/runtime/agent/orchestrator/config.mjs +246 -67
  25. package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
  26. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
  27. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
  28. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
  29. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
  30. package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
  31. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
  36. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  39. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
  41. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
  42. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
  43. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
  44. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
  46. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
  48. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
  49. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
  50. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
  51. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
  52. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  53. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
  54. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  55. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
  56. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
  57. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  58. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
  59. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
  60. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  61. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
  62. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  63. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  64. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
  65. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
  66. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
  67. package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
  70. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
  71. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  72. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
  73. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
  74. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
  75. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  76. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
  77. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
  78. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
  79. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
  80. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
  81. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
  82. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
  83. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
  84. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
  85. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
  87. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
  88. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
  89. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
  90. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
  91. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  92. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
  93. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
  94. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  95. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
  96. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
  97. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
  99. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
  100. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
  101. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
  102. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
  104. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  105. package/src/runtime/channels/lib/config.mjs +34 -34
  106. package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
  107. package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
  108. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  109. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  110. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  111. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  112. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  113. package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
  114. package/src/runtime/channels/lib/scheduler.mjs +2 -2
  115. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  116. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  117. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  118. package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
  119. package/src/runtime/channels/lib/worker-main.mjs +38 -38
  120. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  121. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  122. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  123. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/memory/index.mjs +76 -248
  126. package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
  127. package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
  128. package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
  129. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
  130. package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
  131. package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
  132. package/src/runtime/memory/lib/http-router.mjs +9 -6
  133. package/src/runtime/memory/lib/ko-morph.mjs +3 -20
  134. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
  135. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  136. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
  137. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
  138. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
  139. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  140. package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
  141. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  142. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  143. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  144. package/src/runtime/memory/lib/memory.mjs +1 -1
  145. package/src/runtime/memory/lib/pg/process.mjs +32 -9
  146. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  147. package/src/runtime/memory/tool-defs.mjs +1 -5
  148. package/src/runtime/search/index.mjs +2 -2
  149. package/src/runtime/search/tool-defs.mjs +2 -2
  150. package/src/runtime/shared/abort-race.mjs +70 -0
  151. package/src/runtime/shared/agent-route-config.mjs +119 -0
  152. package/src/runtime/shared/atomic-file.mjs +80 -4
  153. package/src/runtime/shared/channel-notification-routing.mjs +2 -2
  154. package/src/runtime/shared/child-guardian.mjs +204 -108
  155. package/src/runtime/shared/child-spawn-gate.mjs +139 -72
  156. package/src/runtime/shared/config.mjs +120 -47
  157. package/src/runtime/shared/llm/index.mjs +1 -1
  158. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  159. package/src/runtime/shared/resource-admission.mjs +150 -100
  160. package/src/runtime/shared/stream-progress.mjs +6 -0
  161. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  162. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  163. package/src/runtime/shared/tool-surface.mjs +91 -27
  164. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  165. package/src/runtime/shared/turn-snapshot.mjs +47 -6
  166. package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
  167. package/src/session-runtime/channel-config-api.mjs +5 -5
  168. package/src/session-runtime/config-helpers.mjs +39 -37
  169. package/src/session-runtime/config-lifecycle.mjs +27 -27
  170. package/src/session-runtime/lifecycle-api.mjs +22 -3
  171. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  172. package/src/session-runtime/model-route-api.mjs +7 -5
  173. package/src/session-runtime/prewarm.mjs +1 -1
  174. package/src/session-runtime/provider-auth-api.mjs +0 -7
  175. package/src/session-runtime/provider-models.mjs +1 -1
  176. package/src/session-runtime/remote-control.mjs +6 -7
  177. package/src/session-runtime/runtime-core.mjs +63 -39
  178. package/src/session-runtime/runtime-tunables.mjs +4 -4
  179. package/src/session-runtime/self-update.mjs +1 -1
  180. package/src/session-runtime/session-lifecycle.mjs +45 -17
  181. package/src/session-runtime/session-title.mjs +1 -1
  182. package/src/session-runtime/session-turn-api.mjs +96 -24
  183. package/src/session-runtime/settings-api.mjs +12 -24
  184. package/src/session-runtime/tool-defs.mjs +3 -3
  185. package/src/session-runtime/workflow-agents-api.mjs +55 -81
  186. package/src/session-runtime/workflow.mjs +14 -24
  187. package/src/standalone/agent-dispatch-broker.mjs +198 -0
  188. package/src/standalone/agent-tool/helpers.mjs +2 -2
  189. package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
  190. package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
  191. package/src/standalone/agent-tool/tool-def.mjs +0 -14
  192. package/src/standalone/channel-admin.mjs +53 -64
  193. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
  194. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
  195. package/src/standalone/channel-worker.mjs +124 -647
  196. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
  197. package/src/standalone/explore-tool.mjs +187 -70
  198. package/src/standalone/fair-call-scheduler.mjs +264 -0
  199. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  200. package/src/standalone/session-client.mjs +1134 -0
  201. package/src/standalone/session-protocol.mjs +192 -0
  202. package/src/standalone/session-runtime-pool.mjs +393 -0
  203. package/src/standalone/session-runtime-record.mjs +20 -0
  204. package/src/standalone/session-runtime-worker.mjs +203 -0
  205. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
  206. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
  207. package/src/standalone/session-wire.mjs +55 -0
  208. package/src/tui/App.jsx +34 -21
  209. package/src/tui/app/app-format.mjs +1 -1
  210. package/src/tui/app/app-view.jsx +5 -2
  211. package/src/tui/app/channel-pickers.mjs +41 -41
  212. package/src/tui/app/core-memory-picker.mjs +19 -19
  213. package/src/tui/app/doctor.mjs +5 -11
  214. package/src/tui/app/input-parsers.mjs +2 -2
  215. package/src/tui/app/message-selector.mjs +103 -0
  216. package/src/tui/app/onboarding-steps.mjs +3 -4
  217. package/src/tui/app/project-picker.mjs +80 -56
  218. package/src/tui/app/prompt-submit.mjs +48 -43
  219. package/src/tui/app/resume-picker.mjs +2 -2
  220. package/src/tui/app/route-pickers.mjs +3 -1
  221. package/src/tui/app/settings-picker.mjs +31 -33
  222. package/src/tui/app/transcript-window.mjs +38 -0
  223. package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
  224. package/src/tui/app/use-prompt-handlers.mjs +86 -21
  225. package/src/tui/app/use-prompt-hint.mjs +3 -2
  226. package/src/tui/app/use-prompt-queue-history.mjs +10 -3
  227. package/src/tui/app/use-transcript-scroll.mjs +10 -3
  228. package/src/tui/app/use-transcript-window.mjs +50 -17
  229. package/src/tui/components/PromptInput.jsx +71 -38
  230. package/src/tui/components/Spinner.jsx +1 -1
  231. package/src/tui/components/ToolExecution.jsx +1 -1
  232. package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
  233. package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
  234. package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
  235. package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
  236. package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
  237. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  238. package/src/tui/dist/index.mjs +1031 -1676
  239. package/src/tui/hooks/useSession.mjs +16 -0
  240. package/src/tui/index.jsx +12 -13
  241. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  242. package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
  243. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  244. package/src/tui/{engine → session}/context-state.mjs +3 -3
  245. package/src/tui/{engine → session}/labels.mjs +2 -2
  246. package/src/tui/{engine → session}/live-share.mjs +38 -4
  247. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  248. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  249. package/src/tui/session/oauth-flows.mjs +140 -0
  250. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  251. package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
  252. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  253. package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
  254. package/src/tui/{engine → session}/session-api.mjs +110 -93
  255. package/src/tui/{engine → session}/session-flow.mjs +8 -2
  256. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  257. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  258. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  259. package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
  260. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  261. package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
  262. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  263. package/src/tui/{engine → session}/turn.mjs +38 -176
  264. package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
  265. package/src/tui/session.mjs +22 -0
  266. package/src/tui/spinner-meta.mjs +4 -4
  267. package/src/tui/spinner-verbs.mjs +51 -3
  268. package/src/ui/statusline-segments.mjs +1 -1
  269. package/src/ui/statusline.mjs +1 -1
  270. package/vendor/ink/build/components/App.js +10 -1
  271. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  272. package/src/standalone/engine-daemon-client.mjs +0 -894
  273. package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
  274. package/src/standalone/engine-daemon-protocol.mjs +0 -33
  275. package/src/tui/engine/turn-watchdog.mjs +0 -92
  276. package/src/tui/engine.mjs +0 -22
  277. package/src/tui/hooks/useEngine.mjs +0 -16
  278. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  279. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  280. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  281. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  282. /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
@@ -3,6 +3,7 @@ import { createSessionTitleController } from './session-title.mjs';
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
4
4
  import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
5
5
  import { performance } from 'node:perf_hooks';
6
+ import { fileURLToPath } from 'node:url';
6
7
  import keychain from '../lib/keychain-cjs.cjs';
7
8
  import './hitch-profile.mjs';
8
9
  import { ensureStandaloneEnvironment } from '../standalone/seeds.mjs';
@@ -10,8 +11,8 @@ import { createStandaloneAgent } from '../standalone/agent-tool.mjs';
10
11
  import { isAgentOwner } from '../runtime/agent/orchestrator/agent-owner.mjs';
11
12
  import { EXPLORE_TOOL, runExplore } from '../standalone/explore-tool.mjs';
12
13
  import { createStandaloneChannelWorker } from '../standalone/channel-worker.mjs';
13
- import { createStandaloneMemoryRuntime } from '../standalone/memory-runtime-proxy.mjs';
14
14
  import { createStandaloneHookBus } from '../standalone/hook-bus.mjs';
15
+ import { getStandaloneMemoryRuntime } from '../standalone/memory-runtime-proxy.mjs';
15
16
  import { createRemoteTransitionQueue } from './remote-transition-queue.mjs';
16
17
  import { writeLastSessionCwd } from '../runtime/shared/user-cwd.mjs';
17
18
  import { cancelBackgroundTasks } from '../runtime/shared/background-tasks.mjs';
@@ -67,8 +68,8 @@ import {
67
68
  saveTelegramToken,
68
69
  saveSchedule,
69
70
  saveWebhook,
70
- setBackend,
71
- setBackendAsync,
71
+ setChannelProvider,
72
+ setChannelProviderAsync,
72
73
  setScheduleEnabled,
73
74
  setWebhookEnabled,
74
75
  setWebhookConfig,
@@ -260,13 +261,10 @@ import {
260
261
  SEARCH_RUNTIME,
261
262
  SEARCH_TOOL_DEFS,
262
263
  MEMORY_TOOL_DEFS,
263
- MEMORY_RUNTIME,
264
264
  CHANNEL_TOOL_DEFS,
265
- CHANNEL_WORKER_ENTRY,
266
265
  CODE_GRAPH_TOOL_DEFS,
267
266
  CODE_GRAPH_RUNTIME,
268
267
  STATUSLINE_SESSION_ROUTES,
269
- SESSION_RUNTIME_DIR,
270
268
  STANDALONE_SOURCE_ROOT,
271
269
  STANDALONE_ROOT,
272
270
  STANDALONE_DATA_DIR,
@@ -297,6 +295,7 @@ const searchCapableFor = makeSearchCapableFor(normalizeSearchProviderId, isSearc
297
295
  const KEYCHAIN_PREWARM_WAIT_MS = 5000;
298
296
 
299
297
  const outputStyleStatus = (dataDir = STANDALONE_DATA_DIR, opts = {}) => outputStyleStatusRaw(STANDALONE_ROOT, dataDir || STANDALONE_DATA_DIR, opts);
298
+ const MEMORY_RUNTIME_ENTRY = fileURLToPath(new URL('../runtime/memory/index.mjs', import.meta.url));
300
299
  // Workflow/agent pack loaders bound to this runtime's root/data layout.
301
300
  const {
302
301
  listWorkflowPacks,
@@ -417,28 +416,30 @@ export async function createMixdogSessionRuntime({
417
416
  // seconds. Timer is unref'd and first fires after CLEANUP_INITIAL_DELAY_MS
418
417
  // (5min), so this adds zero boot-path cost.
419
418
  try { mgr.startIdleCleanup?.(); } catch { /* cleanup is best-effort */ }
420
- const memoryRuntime = createStandaloneMemoryRuntime({
421
- // Entry constants are module-relative ('../runtime/...'); resolve against
422
- // this module's dir, not STANDALONE_ROOT, or the 'src/' segment is lost.
423
- entry: join(SESSION_RUNTIME_DIR, MEMORY_RUNTIME),
424
- dataDir: pluginDataDir,
425
- cwd,
426
- });
427
419
  rt.memoryModPromise = null;
428
420
  rt.searchModPromise = null;
429
421
  rt.codeGraphModPromise = null;
430
422
 
431
- // Memory module is always-on. `memoryEnabled()` is kept as a thin alias that
432
- // now always returns true (callers/compaction helpers still reference it);
433
- // the user-facing toggle is `recap` (background cycles only), read via
434
- // recapEnabled(config).
435
- const memoryEnabled = () => true;
423
+ // Memory is always-on. The user-facing toggle is `recap` (background cycles
424
+ // only), read via recapEnabled(config).
436
425
  const recapEnabledFn = () => recapEnabled(rt.config, true);
437
426
  const channelsEnabled = () => moduleEnabled(rt.config, 'channels', true);
438
427
 
439
428
  async function getMemoryModule() {
440
429
  const startedAt = performance.now();
441
- rt.memoryModPromise ??= Promise.resolve(memoryRuntime);
430
+ rt.memoryModPromise ??= Promise.resolve().then(() => {
431
+ const runtime = getStandaloneMemoryRuntime({
432
+ entry: MEMORY_RUNTIME_ENTRY,
433
+ dataDir: process.env.MIXDOG_DATA_DIR || cfgMod.getPluginData?.() || STANDALONE_DATA_DIR,
434
+ });
435
+ // Session teardown must never stop a process shared by every other live
436
+ // session. The daemon owns the actual stop()/deregister lifecycle.
437
+ return {
438
+ init: () => runtime.init(),
439
+ handleToolCall: (...args) => runtime.handleToolCall(...args),
440
+ buildSessionCoreMemoryPayload: (...args) => runtime.buildSessionCoreMemoryPayload(...args),
441
+ };
442
+ });
442
443
  const mod = await rt.memoryModPromise;
443
444
  if (typeof mod?.init === 'function') {
444
445
  await mod.init();
@@ -469,10 +470,6 @@ export async function createMixdogSessionRuntime({
469
470
 
470
471
  const nextConfig = { ...(rt.config || {}) };
471
472
  nextConfig.presets = upsertWorkflowPreset(nextConfig.presets, 'lead', leadRoute);
472
- nextConfig.workflowRoutes = {
473
- ...(nextConfig.workflowRoutes || {}),
474
- lead: leadRoute,
475
- };
476
473
  nextConfig.default = workflowPresetId('lead');
477
474
 
478
475
  saveConfigAndAdopt(nextConfig);
@@ -534,6 +531,20 @@ export async function createMixdogSessionRuntime({
534
531
  channelStartPromise: null,
535
532
  };
536
533
  rt.activeTurnCount = 0;
534
+ rt.activeTurnAbortControllers = new Set();
535
+ const registerActiveTurnController = (controller) => {
536
+ rt.activeTurnAbortControllers.add(controller);
537
+ return () => rt.activeTurnAbortControllers.delete(controller);
538
+ };
539
+ const abortActiveTurns = (reason) => {
540
+ let aborted = false;
541
+ for (const controller of [...rt.activeTurnAbortControllers]) {
542
+ if (controller.signal.aborted) continue;
543
+ aborted = true;
544
+ try { controller.abort(reason); } catch {}
545
+ }
546
+ return aborted;
547
+ };
537
548
  rt.firstTurnCompleted = false;
538
549
  function hookTranscriptPath(sessionId) {
539
550
  const id = clean(sessionId);
@@ -746,7 +757,6 @@ export async function createMixdogSessionRuntime({
746
757
  };
747
758
  const channelsStartedAt = performance.now();
748
759
  const channels = createStandaloneChannelWorker({
749
- entry: join(SESSION_RUNTIME_DIR, CHANNEL_WORKER_ENTRY),
750
760
  rootDir: STANDALONE_ROOT,
751
761
  dataDir: cfgMod.getPluginData(),
752
762
  cwd,
@@ -971,8 +981,8 @@ export async function createMixdogSessionRuntime({
971
981
  adoptConfig,
972
982
  saveConfigAndAdopt,
973
983
  flushConfigSave,
974
- flushBackendSave,
975
- scheduleBackendSave,
984
+ flushChannelProviderSave,
985
+ scheduleChannelProviderSave,
976
986
  scheduleSkillsSave,
977
987
  flushSkillsSave,
978
988
  flushOutputStyleSave,
@@ -992,8 +1002,8 @@ export async function createMixdogSessionRuntime({
992
1002
  getRoute: () => rt.route,
993
1003
  cfgMod,
994
1004
  sharedCfgMod,
995
- setBackend,
996
- setBackendAsync,
1005
+ setChannelProvider,
1006
+ setChannelProviderAsync,
997
1007
  setConfiguredShell,
998
1008
  normalizeSystemShellConfig,
999
1009
  normalizeSearchRouteConfig,
@@ -1198,7 +1208,6 @@ export async function createMixdogSessionRuntime({
1198
1208
  reg,
1199
1209
  cfgMod,
1200
1210
  activeWorkflowContext,
1201
- memoryEnabled,
1202
1211
  hooks,
1203
1212
  hookCommonPayload,
1204
1213
  mcpClient,
@@ -1253,7 +1262,7 @@ export async function createMixdogSessionRuntime({
1253
1262
  channels,
1254
1263
  channelsEnabled,
1255
1264
  hasActiveAutomation,
1256
- flushBackendSave,
1265
+ flushChannelProviderSave,
1257
1266
  invokeChannelStart,
1258
1267
  createCurrentSession,
1259
1268
  ensureRemoteTranscriptWriter,
@@ -1304,7 +1313,7 @@ export async function createMixdogSessionRuntime({
1304
1313
  // was toggled into) remote mode. Non-remote sessions never contend for the
1305
1314
  // channel; see startRemote()/stopRemote() and the `/remote` toggle.
1306
1315
  // A manual `mixdog --remote` request is deferred past the TUI's
1307
- // first frame. startRemote() front-loads heavy work — memory daemon fork
1316
+ // first frame. startRemote() front-loads memory initialization
1308
1317
  // (PG + forced ONNX embed warmup in the child), eager session create, and
1309
1318
  // the channel-worker fork — and running it inline here interleaves that
1310
1319
  // CPU/disk load with engine boot, visibly delaying the first ink frame by
@@ -1323,7 +1332,7 @@ export async function createMixdogSessionRuntime({
1323
1332
  } else {
1324
1333
  // Automation decoupling (user decision): enabled schedules/webhooks boot
1325
1334
  // the worker on their own — no remote flag, no remote.autoStart, and no
1326
- // messaging backend. A later explicit Remote ON promotes the same
1335
+ // messaging provider. A later explicit Remote ON promotes the same
1327
1336
  // daemon without restarting schedules or webhooks.
1328
1337
  prewarmTimers.channelStartTimer = setTimeout(() => {
1329
1338
  prewarmTimers.channelStartTimer = null;
@@ -1341,7 +1350,7 @@ export async function createMixdogSessionRuntime({
1341
1350
 
1342
1351
  // Pure settings-delegate methods (onboarding status/skip, autoClear, profile,
1343
1352
  // compaction, recap/memory, channels, systemShell, update settings, channel
1344
- // token save/forget, setBackend). Extracted to session-runtime/settings-api.mjs
1353
+ // token save/forget, setChannelProvider). Extracted to session-runtime/settings-api.mjs
1345
1354
  // and SPREAD into the API object below so the external surface is unchanged.
1346
1355
  const settingsApi = createSettingsApi({
1347
1356
  getConfig: () => rt.config,
@@ -1350,7 +1359,7 @@ export async function createMixdogSessionRuntime({
1350
1359
  getRemoteEnabled: () => rt.remoteEnabled,
1351
1360
  adoptConfig,
1352
1361
  saveConfigAndAdopt,
1353
- scheduleBackendSave,
1362
+ scheduleChannelProviderSave,
1354
1363
  scheduleSkillsSave,
1355
1364
  cfgMod,
1356
1365
  hasOwn,
@@ -1368,7 +1377,6 @@ export async function createMixdogSessionRuntime({
1368
1377
  parseDurationMs,
1369
1378
  formatDurationMs,
1370
1379
  localPackageVersion,
1371
- memoryEnabled,
1372
1380
  recapEnabledFn,
1373
1381
  channelsEnabled,
1374
1382
  autoUpdateEnabled,
@@ -1392,11 +1400,11 @@ export async function createMixdogSessionRuntime({
1392
1400
  forgetDiscordToken,
1393
1401
  saveTelegramToken,
1394
1402
  forgetTelegramToken,
1395
- setBackend,
1403
+ setChannelProvider,
1396
1404
  });
1397
1405
 
1398
1406
  const channelConfigApi = createChannelConfigApi({
1399
- flushBackendSave,
1407
+ flushChannelProviderSave,
1400
1408
  channels,
1401
1409
  reloadChannelsSoon,
1402
1410
  // Automation saved mid-session boots the worker (claim-if-vacant) even
@@ -1445,6 +1453,8 @@ export async function createMixdogSessionRuntime({
1445
1453
  getMemoryModPromise: () => rt.memoryModPromise,
1446
1454
  setMemoryModPromise: (v) => { rt.memoryModPromise = v; },
1447
1455
  setSessionNeedsCwdRefresh: (v) => { rt.sessionNeedsCwdRefresh = v; },
1456
+ getReservedSessionId: () => rt.reservedSessionId,
1457
+ abortActiveTurns,
1448
1458
  hooks,
1449
1459
  hookCommonPayload,
1450
1460
  mgr,
@@ -1456,7 +1466,7 @@ export async function createMixdogSessionRuntime({
1456
1466
  warmupTimers,
1457
1467
  prewarmTimers,
1458
1468
  flushConfigSave,
1459
- flushBackendSave,
1469
+ flushChannelProviderSave,
1460
1470
  flushOutputStyleSave,
1461
1471
  flushAllConfigSavesAsync,
1462
1472
  withTeardownDeadline,
@@ -1624,6 +1634,7 @@ export async function createMixdogSessionRuntime({
1624
1634
  mcpTurnGraceMs,
1625
1635
  awaitRoutePreparation: () => routePreparation.wait(),
1626
1636
  getReservedSessionId: () => rt.reservedSessionId,
1637
+ registerActiveTurnController,
1627
1638
  sessionTitles,
1628
1639
  });
1629
1640
 
@@ -1633,7 +1644,11 @@ export async function createMixdogSessionRuntime({
1633
1644
  ...providerAuthApi,
1634
1645
  ...mediaApi,
1635
1646
  // Turn-scoped worktree review plus exact child apply_patch attribution.
1636
- getTurnReviewDiff: () => getTurnSnapshotReviewDiff(rt.currentCwd, rt.session?.id),
1647
+ getTurnReviewDiff: (options = {}) => getTurnSnapshotReviewDiff(
1648
+ rt.currentCwd,
1649
+ rt.session?.id,
1650
+ options,
1651
+ ),
1637
1652
  revertTurnReviewFile: (file) => revertTurnSnapshotReviewFile(
1638
1653
  rt.currentCwd,
1639
1654
  rt.session?.id,
@@ -1651,6 +1666,15 @@ export async function createMixdogSessionRuntime({
1651
1666
  throw new Error(`session ${rt.session.id} is already materialized`);
1652
1667
  }
1653
1668
  rt.reservedSessionId = id;
1669
+ // Reservation is the earliest safe point to prepare keychain, memory,
1670
+ // provider metadata, hooks, and the provider transport. This starts no
1671
+ // model response and therefore incurs no inference/token usage. The first
1672
+ // submit joins this single-flight promise instead of paying cold setup.
1673
+ void createCurrentSession('reservation').catch((error) => {
1674
+ bootProfile('session:reservation-prewarm-failed', {
1675
+ error: error?.message || String(error),
1676
+ });
1677
+ });
1654
1678
  return id;
1655
1679
  },
1656
1680
  get provider() {
@@ -24,10 +24,10 @@ export function readRuntimeTunables() {
24
24
  statuslineUsageRefreshDelayMs: envDelayMs('MIXDOG_STATUSLINE_USAGE_REFRESH_MS', 240_000, { min: 30_000, max: 540_000 }),
25
25
  channelStartDelayMs: envDelayMs('MIXDOG_CHANNEL_START_DELAY_MS', 10_000, { min: 0, max: 120_000 }),
26
26
  backgroundBusyRetryMs: envDelayMs('MIXDOG_BACKGROUND_BUSY_RETRY_MS', 1_000, { min: 50, max: 10_000 }),
27
- // MCP startup must not own the first-token critical path. Servers that do
28
- // not connect inside this small grace period join through the existing
29
- // late-tool announcement path on the next turn.
30
- mcpTurnGraceMs: envDelayMs('MIXDOG_MCP_TURN_GRACE_MS', 150, { min: 0, max: 2_000 }),
27
+ // MCP startup never owns the first-token critical path by default. Already
28
+ // connected tools fold synchronously; in-flight servers join through the
29
+ // late-tool announcement path. Operators may opt back into a grace window.
30
+ mcpTurnGraceMs: envDelayMs('MIXDOG_MCP_TURN_GRACE_MS', 0, { min: 0, max: 2_000 }),
31
31
  // Boot-time remote start is deferred past the first frame; see the caller.
32
32
  remoteAutoStartDelayMs: envDelayMs('MIXDOG_REMOTE_AUTOSTART_DELAY_MS', 1_500, { min: 0, max: 60_000 }),
33
33
  providerWarmupEnabled: !envFlag('MIXDOG_DISABLE_PROVIDER_WARMUP')
@@ -14,7 +14,7 @@ import {
14
14
 
15
15
  const STAGE_POLL_MS = 3_000;
16
16
  const STAGE_POLL_MAX_MS = 10 * 60 * 1000;
17
- // A backend daemon can host many session runtimes. Their boot controllers all
17
+ // A daemon can host many session runtimes. Their boot controllers all
18
18
  // need the same update state, not one forced registry request per pane.
19
19
  const processBootChecks = new Map();
20
20
 
@@ -18,6 +18,7 @@ import { envFlag } from './env.mjs';
18
18
  import { createPrewarmSchedulers } from './prewarm.mjs';
19
19
  import { hasActiveAutomation } from '../standalone/channel-admin.mjs';
20
20
  import { createRemoteTranscript } from './remote-transcript.mjs';
21
+ import { runAbortable, throwIfAborted } from '../runtime/shared/abort-race.mjs';
21
22
  export function createSessionLifecycle({
22
23
  rt,
23
24
  collectProviderModels,
@@ -30,7 +31,6 @@ export function createSessionLifecycle({
30
31
  reg,
31
32
  cfgMod,
32
33
  activeWorkflowContext,
33
- memoryEnabled,
34
34
  hooks,
35
35
  hookCommonPayload,
36
36
  mcpClient,
@@ -62,9 +62,10 @@ export function createSessionLifecycle({
62
62
  codeGraphPrewarmEnabled,
63
63
  prewarmState,
64
64
  }) {
65
- async function resolveMissingRouteModelForFirstTurn() {
65
+ async function resolveMissingRouteModelForFirstTurn(signal = null) {
66
66
  if (routeHasModel()) return rt.route;
67
- const models = await collectProviderModels();
67
+ const models = await runAbortable(signal, () => collectProviderModels());
68
+ throwIfAborted(signal);
68
69
  const picked = models[0] || null;
69
70
  if (!picked) {
70
71
  throw new Error('No provider models available. Open /providers to sign in, then /model to choose a model.');
@@ -78,10 +79,14 @@ export function createSessionLifecycle({
78
79
  return rt.route;
79
80
  }
80
81
 
81
- async function refreshRouteEffort(modelMetaOverride = null, expectedRoute = null) {
82
+ async function refreshRouteEffort(modelMetaOverride = null, expectedRoute = null, signal = null) {
82
83
  const targetRoute = expectedRoute || rt.route;
83
- await ensureProvidersReady(ensureProviderEnabled(rt.config, targetRoute.provider));
84
- const modelMeta = modelMetaOverride || await lookupModelMeta(targetRoute.provider, targetRoute.model);
84
+ await runAbortable(signal, () => ensureProvidersReady(ensureProviderEnabled(rt.config, targetRoute.provider)));
85
+ const modelMeta = modelMetaOverride || await runAbortable(
86
+ signal,
87
+ () => lookupModelMeta(targetRoute.provider, targetRoute.model),
88
+ );
89
+ throwIfAborted(signal);
85
90
  // A rapid second resume/model change can replace the route while provider
86
91
  // metadata is loading. Never let the older completion overwrite it.
87
92
  if (expectedRoute && rt.route !== expectedRoute) return null;
@@ -127,8 +132,12 @@ export function createSessionLifecycle({
127
132
  return await createCurrentSession();
128
133
  }
129
134
 
130
- async function createCurrentSession(reason = 'demand') {
131
- if (rt.sessionCreatePromise) return await rt.sessionCreatePromise;
135
+ async function createCurrentSession(reason = 'demand', options = {}) {
136
+ const signal = options?.signal || null;
137
+ throwIfAborted(signal);
138
+ if (rt.sessionCreatePromise) {
139
+ return await runAbortable(signal, () => rt.sessionCreatePromise, 'Session creation aborted');
140
+ }
132
141
  if (rt.session?.id && !rt.sessionNeedsCwdRefresh) {
133
142
  const liveSession = mgr.getSession(rt.session.id);
134
143
  if (liveSession && liveSession.closed !== true && liveSession.status !== 'closed') {
@@ -140,23 +149,34 @@ export function createSessionLifecycle({
140
149
 
141
150
  const startedAt = performance.now();
142
151
  bootProfile('session:create:start', { mode: rt.mode, reason });
152
+ // A daemon reservation is deliberate user-think-time prewarm. Start the
153
+ // one-time agent-loop module load now so the first real prompt does not pay
154
+ // its ~100ms dynamic-import graph immediately before provider.send.
155
+ if (reason === 'reservation' && typeof mgr.prewarmAgentLoop === 'function') {
156
+ void mgr.prewarmAgentLoop().catch((error) => {
157
+ bootProfile('agent-loop:prewarm-failed', { error: error?.message || String(error) });
158
+ });
159
+ }
143
160
  const promise = (async () => {
144
161
  // Demand-only: this starts only after the user submits (unless an
145
162
  // explicitly enabled prewarm caller asks for a session). Core-memory
146
163
  // startup does not depend on keychain/provider readiness, so overlap the
147
164
  // two cold paths instead of paying their bounded waits serially.
148
- const coreMemoryContextPromise = loadCoreMemoryContext();
149
- await awaitKeychainPrewarm();
165
+ const coreMemoryContextPromise = Promise.resolve(loadCoreMemoryContext());
166
+ coreMemoryContextPromise.catch(() => {});
167
+ await runAbortable(signal, () => awaitKeychainPrewarm());
150
168
  ensureConfigForRouteProvider();
151
- await resolveMissingRouteModelForFirstTurn();
169
+ await resolveMissingRouteModelForFirstTurn(signal);
152
170
  requireModelRoute();
153
171
  bootProfile('session:create:route-ready', { ms: (performance.now() - startedAt).toFixed(1) });
154
172
  // Route effort waits on provider readiness while the already-started
155
173
  // memory load continues independently.
156
- const [, coreMemoryContext] = await Promise.all([
157
- refreshRouteEffort(),
174
+ const expectedRoute = rt.route;
175
+ const [, coreMemoryContext] = await runAbortable(signal, () => Promise.all([
176
+ refreshRouteEffort(null, expectedRoute, signal),
158
177
  coreMemoryContextPromise,
159
- ]);
178
+ ]));
179
+ throwIfAborted(signal);
160
180
  bootProfile('session:create:effort-ready', { ms: (performance.now() - startedAt).toFixed(1) });
161
181
  const providerImpl = reg.getProvider(rt.route.provider);
162
182
  if (!providerImpl) {
@@ -164,6 +184,7 @@ export function createSessionLifecycle({
164
184
  }
165
185
  bootProfile('session:create:provider-ready', { ms: (performance.now() - startedAt).toFixed(1) });
166
186
  if (rt.closeRequested) throw new Error('runtime is closing');
187
+ throwIfAborted(signal);
167
188
  const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
168
189
  // Load the active WORKFLOW.md pack once for both summary + context block.
169
190
  const { summary: workflow, context: workflowContext } = activeWorkflowContext(rt.config, dataDir);
@@ -187,7 +208,7 @@ export function createSessionLifecycle({
187
208
  workflowContext,
188
209
  fast: rt.route.fast === true,
189
210
  compaction: rt.config.compaction && typeof rt.config.compaction === 'object'
190
- ? normalizeCompactionConfig(rt.config.compaction, { memoryEnabled: memoryEnabled() })
211
+ ? normalizeCompactionConfig(rt.config.compaction)
191
212
  : undefined,
192
213
  };
193
214
  if (hasOwn(rt.route, 'effort') || rt.route.effectiveEffort) {
@@ -229,7 +250,10 @@ export function createSessionLifecycle({
229
250
  const startSource = /resume/i.test(String(reason || ''))
230
251
  ? 'resume'
231
252
  : (/clear/i.test(String(reason || '')) ? 'clear' : 'startup');
232
- const startDispatch = await hooks.dispatch('SessionStart', hookCommonPayload({ session_id: rt.session.id, source: startSource, model: rt.route.model }));
253
+ const startDispatch = await runAbortable(
254
+ signal,
255
+ () => hooks.dispatch('SessionStart', hookCommonPayload({ session_id: rt.session.id, source: startSource, model: rt.route.model })),
256
+ );
233
257
  const startContext = Array.isArray(startDispatch?.additionalContext)
234
258
  ? startDispatch.additionalContext.join('\n\n')
235
259
  : String(startDispatch?.additionalContext || '');
@@ -238,7 +262,11 @@ export function createSessionLifecycle({
238
262
  rt.session.messages.push({ role: 'assistant', content: '.' });
239
263
  rt.session.updatedAt = Date.now();
240
264
  }
241
- } catch { /* best-effort: never break session create */ }
265
+ } catch {
266
+ throwIfAborted(signal);
267
+ // best-effort: ordinary hook failure never breaks session create
268
+ }
269
+ throwIfAborted(signal);
242
270
  bootProfile('session:create:ready', {
243
271
  ms: (performance.now() - startedAt).toFixed(1),
244
272
  reason,
@@ -106,7 +106,7 @@ export function resolvedSystemLocale() {
106
106
  }
107
107
  }
108
108
 
109
- /** Shared backend controller used by every lead surface (TUI and desktop). */
109
+ /** Shared session controller used by every lead surface (TUI and desktop). */
110
110
  export function createSessionTitleController(deps = {}) {
111
111
  const firstAttempts = new Set();
112
112
  const thirdAttempts = new Set();