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
@@ -1,12 +1,12 @@
1
1
  /**
2
- * src/tui/engine-local-session.mjs - daemon-hosted local session store.
2
+ * src/tui/session-local.mjs - daemon-hosted local session store.
3
3
  *
4
- * Runs mixdog's session manager inside the singleton backend and exposes the
4
+ * Runs mixdog's session manager inside the singleton daemon and exposes the
5
5
  * subscribable store projected to clients. React/ink consumes the matching
6
- * remote view through useSyncExternalStore (see hooks/useEngine.mjs).
6
+ * remote view through useSyncExternalStore (see hooks/useSession.mjs).
7
7
  *
8
- * Product TUI/CLI callers use ./engine.mjs and never load this module directly.
9
- * The singleton backend daemon and explicit parity tests are the only callers.
8
+ * Product TUI/CLI callers use ./session.mjs and never load this module directly.
9
+ * The singleton daemon and explicit parity tests are the only callers.
10
10
  */
11
11
  import { performance } from 'node:perf_hooks';
12
12
  import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync, watch, writeFileSync } from 'node:fs';
@@ -29,8 +29,8 @@ import { presentErrorText } from '../runtime/shared/err-text.mjs';
29
29
  import { sessionPath } from '../runtime/agent/orchestrator/session/store/paths-heartbeat.mjs';
30
30
  import { listThemes, getThemeSetting, setThemeSetting } from './theme.mjs';
31
31
  import { resetAllStreamingMarkdownStablePrefixes } from './markdown/streaming-markdown.mjs';
32
- import { bootProfile } from './engine/boot-profile.mjs';
33
- import { createSessionStats, applyUsageDelta } from './engine/session-stats.mjs';
32
+ import { bootProfile } from './session/boot-profile.mjs';
33
+ import { createSessionStats, applyUsageDelta } from './session/session-stats.mjs';
34
34
  import {
35
35
  pickVerb,
36
36
  pickDoneVerb,
@@ -38,27 +38,27 @@ import {
38
38
  compactEventLabel,
39
39
  compactEventDetail,
40
40
  projectNameFromPath,
41
- } from './engine/labels.mjs';
42
- import { polishNoticeText } from './engine/notice-text.mjs';
41
+ } from './session/labels.mjs';
42
+ import { polishNoticeText } from './session/notice-text.mjs';
43
43
  import {
44
44
  toolResultText,
45
45
  toolAggregateDetailFallback,
46
46
  toolGroupedDisplayFallback,
47
47
  toolErrorDisplay,
48
- } from './engine/tool-result-text.mjs';
48
+ } from './session/tool-result-text.mjs';
49
49
  import {
50
50
  toolCallId,
51
51
  toolResultCallId,
52
52
  toolCallName,
53
53
  toolCallArgs,
54
- } from './engine/tool-call-fields.mjs';
54
+ } from './session/tool-call-fields.mjs';
55
55
  import {
56
56
  buildExecutionResponseToolItem,
57
57
  parseBackgroundTaskEnvelope,
58
58
  parseSyntheticAgentMessage,
59
59
  toolResultStatus,
60
60
  isErrorToolStatus,
61
- } from './engine/agent-envelope.mjs';
61
+ } from './session/agent-envelope.mjs';
62
62
  import {
63
63
  queuePriorityValue,
64
64
  defaultQueuePriority,
@@ -72,50 +72,50 @@ import {
72
72
  mergePastedImages,
73
73
  mergePastedTexts,
74
74
  callCommitCallbacks,
75
- } from './engine/queue-helpers.mjs';
75
+ } from './session/queue-helpers.mjs';
76
76
  import {
77
77
  resolveTuiRuntimeNotificationDelivery,
78
- } from './engine/notification-plan.mjs';
78
+ } from './session/notification-plan.mjs';
79
79
  import {
80
80
  TUI_FRAME_MS,
81
81
  cancelRenderAlignedStoreFlush,
82
82
  scheduleRenderAlignedStoreFlush,
83
83
  yieldToRenderer,
84
- } from './engine/render-timing.mjs';
84
+ } from './session/render-timing.mjs';
85
85
  import {
86
86
  aggregateRawResult,
87
87
  aggregateBucketForCategory,
88
88
  aggregateSummaries,
89
89
  assignAggregateSummaryOrder,
90
- } from './engine/tool-result-status.mjs';
91
- import { createToolApproval } from './engine/tool-approval.mjs';
92
- import { createToolCardResults } from './engine/tool-card-results.mjs';
93
- import { createAgentJobFeed } from './engine/agent-job-feed.mjs';
94
- import { appendAgentResponseTail } from './engine/agent-response-tail.mjs';
90
+ } from './session/tool-result-status.mjs';
91
+ import { createToolApproval } from './session/tool-approval.mjs';
92
+ import { createToolCardResults } from './session/tool-card-results.mjs';
93
+ import { createAgentJobFeed } from './session/agent-job-feed.mjs';
94
+ import { appendAgentResponseTail } from './session/agent-response-tail.mjs';
95
95
  import {
96
96
  appendTuiSteeringPersist,
97
97
  dropTuiSteeringPersist,
98
98
  drainTuiSteeringPersist,
99
99
  flushTuiSteeringPersist,
100
- } from './engine/tui-steering-persist.mjs';
101
- import { createContextState } from './engine/context-state.mjs';
102
- import { recomputePromptHistory } from './engine/prompt-history.mjs';
100
+ } from './session/tui-steering-persist.mjs';
101
+ import { createContextState } from './session/context-state.mjs';
102
+ import { recomputePromptHistory } from './session/prompt-history.mjs';
103
103
  import {
104
104
  appendPromptHistory,
105
105
  buildMergedPromptHistory,
106
106
  loadPromptHistory,
107
107
  } from './prompt-history-store.mjs';
108
- import { createSessionFlow } from './engine/session-flow.mjs';
109
- import { createRunTurn } from './engine/turn.mjs';
110
- import { createEngineApi } from './engine/session-api.mjs';
111
- import { createFrameBatchedStorePublisher } from './engine/frame-batched-store.mjs';
112
- import { createLiveShare, liveSharePipePath } from './engine/live-share.mjs';
108
+ import { createSessionFlow } from './session/session-flow.mjs';
109
+ import { createRunTurn } from './session/turn.mjs';
110
+ import { createSessionApi } from './session/session-api.mjs';
111
+ import { createFrameBatchedStorePublisher } from './session/frame-batched-store.mjs';
112
+ import { createLiveShare, forwardViewerSubmit, liveSharePipePath } from './session/live-share.mjs';
113
113
  import { displayModelName } from '../ui/model-display.mjs';
114
114
 
115
115
  const SESSION_RUNTIME_MODULE = '../mixdog-session-runtime.mjs';
116
116
 
117
117
  // The runtime graph is imported lazily, but that import (measured ~250ms) used
118
- // to land inside the FIRST engine creation, which desktop performs while it
118
+ // to land inside the FIRST session runtime creation, which desktop performs while it
119
119
  // holds its transition lock — so the user's first navigation paid for it. Hosts
120
120
  // can start it during their own idle startup instead; the promise is shared, so
121
121
  // the create path either awaits an in-flight preload or does the import itself.
@@ -132,6 +132,17 @@ export function preloadSessionRuntimeModule() {
132
132
  });
133
133
  }
134
134
 
135
+ let agentLoopPrewarmPromise = null;
136
+ export function preloadAgentLoopRuntime() {
137
+ agentLoopPrewarmPromise ??= import(
138
+ '../runtime/agent/orchestrator/session/manager/runtime-loaders.mjs'
139
+ ).then((module) => module.prewarmAgentLoop());
140
+ void agentLoopPrewarmPromise.catch(() => {
141
+ // Reservation-time prewarm retries through the same shared loader.
142
+ agentLoopPrewarmPromise = null;
143
+ });
144
+ }
145
+
135
146
  // Windows keychain reads go through a DPAPI PowerShell host whose cold start
136
147
  // dominates a packaged boot (measured ~1.5s). The runtime batches every secret
137
148
  // into one call, but it only starts that batch when the runtime is created —
@@ -155,26 +166,14 @@ const TOOL_APPROVAL_TIMEOUT_MS = (() => {
155
166
  return Number.isFinite(value) && value > 0 ? Math.max(1000, Math.round(value)) : 120_000;
156
167
  })();
157
168
 
158
- // Idle cap for a single lead TUI turn: the timer is reset by observable model,
159
- // tool, usage, stage, compact, or steering progress. A provider call that stops
160
- // producing progress otherwise leaves runTurn holding busy=true with no unwind,
161
- // so submit() only ever queues and the UI is permanently input-dead. On trip we
162
- // abort through the SAME interrupt path Esc uses; if that does not unwind, a
163
- // short grace path force-releases busy=false and drains the queue. Default is
164
- // intentionally finite (5min). Env-overridable for long local experiments.
165
- const LEAD_TURN_TIMEOUT_MS = (() => {
166
- const value = Number(process.env.MIXDOG_LEAD_TURN_TIMEOUT_MS);
167
- return Number.isFinite(value) && value > 0 ? Math.max(10_000, Math.round(value)) : 5 * 60_000;
168
- })();
169
-
170
169
  // Opt-in diagnostic trace for the hang chain (runTurn start/end, busy-queue
171
- // enqueue/drain, watchdog trip). Quiet by default so it can never tear through
170
+ // enqueue/drain). Quiet by default so it can never tear through
172
171
  // the alternate-screen render; enable with MIXDOG_TUI_DEBUG=1.
173
- import { tuiDebug, nextId, cleanupStaleTranscriptSpillDirs, createTranscriptSpillBuffer, refillTranscriptViewOverlap, replaceEngineItemsState, createEngineItemMutators, TRANSCRIPT_LIVE_ITEM_CAP, TRANSCRIPT_SPILL_CHUNK_ITEMS } from './engine/transcript-spill.mjs';
174
- export { cleanupStaleTranscriptSpillDirs, createTranscriptSpillBuffer, refillTranscriptViewOverlap, replaceEngineItemsState, createEngineItemMutators, TRANSCRIPT_LIVE_ITEM_CAP, TRANSCRIPT_SPILL_CHUNK_ITEMS } from './engine/transcript-spill.mjs';
175
- export { parseBackgroundTaskEnvelope } from './engine/agent-envelope.mjs';
172
+ import { tuiDebug, nextId, cleanupStaleTranscriptSpillDirs, createTranscriptSpillBuffer, refillTranscriptViewOverlap, replaceSessionItemsState, createSessionItemMutators, TRANSCRIPT_LIVE_ITEM_CAP, TRANSCRIPT_SPILL_CHUNK_ITEMS } from './session/transcript-spill.mjs';
173
+ export { cleanupStaleTranscriptSpillDirs, createTranscriptSpillBuffer, refillTranscriptViewOverlap, replaceSessionItemsState, createSessionItemMutators, TRANSCRIPT_LIVE_ITEM_CAP, TRANSCRIPT_SPILL_CHUNK_ITEMS } from './session/transcript-spill.mjs';
174
+ export { parseBackgroundTaskEnvelope } from './session/agent-envelope.mjs';
176
175
 
177
- export async function createLocalEngineSession({
176
+ export async function createLocalSessionRuntime({
178
177
  provider: providerName,
179
178
  model,
180
179
  toolMode = 'full',
@@ -183,7 +182,7 @@ export async function createLocalEngineSession({
183
182
  desktopSession,
184
183
  } = {}) {
185
184
  const startedAt = performance.now();
186
- bootProfile('engine:create:start', { provider: providerName, model, toolMode, remote });
185
+ bootProfile('session:create:start', { provider: providerName, model, toolMode, remote });
187
186
  // Silence provider/session diagnostics so they cannot tear through the
188
187
  // alternate-screen React/ink render.
189
188
  process.env.MIXDOG_QUIET_PROVIDER_LOG = '1';
@@ -203,7 +202,7 @@ export async function createLocalEngineSession({
203
202
  ...(cwd ? { cwd } : {}),
204
203
  ...(desktopSession ? { desktopSession } : {}),
205
204
  });
206
- bootProfile('engine:create:runtime-ready', { ms: (performance.now() - startedAt).toFixed(1) });
205
+ bootProfile('session:create:runtime-ready', { ms: (performance.now() - startedAt).toFixed(1) });
207
206
  const runtimeCwd = runtime.cwd || process.cwd();
208
207
  const stateStartedAt = performance.now();
209
208
  const flags = {
@@ -224,7 +223,7 @@ export async function createLocalEngineSession({
224
223
  const displayedExecutionNotificationKeys = new Set();
225
224
  const bag = {};
226
225
  let state;
227
- // Route/context/agent-status derivations live in ./engine/context-state.mjs.
226
+ // Route/context/agent-status derivations live in ./session/context-state.mjs.
228
227
  // getState()/getPendingSessionReset() are late-bound to the `state` and
229
228
  // `pendingSessionReset` closures declared below; the sync helpers mutate
230
229
  // state.stats / display fields IN PLACE exactly as the old inline versions
@@ -266,7 +265,7 @@ export async function createLocalEngineSession({
266
265
  toolApproval: null,
267
266
  lastTurn: null,
268
267
  stats: createSessionStats(),
269
- // Incremental derivations published by the engine so App does not scan all
268
+ // Incremental derivations published by the session runtime so App does not scan all
270
269
  // transcript items on every change:
271
270
  // - activeToolSummary: running Explore/Search active counts + earliest
272
271
  // startedAt for the prompt-line status (replaces App.jsx O(n) items scan).
@@ -285,17 +284,17 @@ export async function createLocalEngineSession({
285
284
  cwd: runtimeCwd,
286
285
  themeEpoch: 0,
287
286
  };
288
- bootProfile('engine:route-state-ready', { ms: (performance.now() - stateStartedAt).toFixed(1) });
289
- bootProfile('engine:state-ready', { ms: (performance.now() - stateStartedAt).toFixed(1) });
287
+ bootProfile('session:route-state-ready', { ms: (performance.now() - stateStartedAt).toFixed(1) });
288
+ bootProfile('session:state-ready', { ms: (performance.now() - stateStartedAt).toFixed(1) });
290
289
  const contextStartedAt = performance.now();
291
290
  syncContextStats({ allowEstimated: true });
292
- bootProfile('engine:context-ready', { ms: (performance.now() - contextStartedAt).toFixed(1) });
291
+ bootProfile('session:context-ready', { ms: (performance.now() - contextStartedAt).toFixed(1) });
293
292
  const listeners = new Set();
294
293
  // React/useSyncExternalStore reads only this immutable published snapshot.
295
- // `state` remains the engine's synchronous draft until a frame flush swaps
294
+ // `state` remains the session runtime's synchronous draft until a frame flush swaps
296
295
  // the complete draft (including its single revision bump) into this slot.
297
296
  let publishedState = process.env.NODE_ENV === 'production' ? state : Object.freeze(state);
298
- // The mutable engine draft must never be the object exposed to React.
297
+ // The mutable session runtime draft must never be the object exposed to React.
299
298
  state = { ...state, stats: { ...state.stats } };
300
299
  // Mutations stay synchronous, but React publications are frame-coalesced.
301
300
  // structureRevision is committed by the publisher exactly once immediately
@@ -305,7 +304,7 @@ export async function createLocalEngineSession({
305
304
  publishState: (next) => {
306
305
  publishedState = process.env.NODE_ENV === 'production' ? next : Object.freeze(next);
307
306
  // Detach the next draft, including the only intentionally mutable nested
308
- // record, so legacy/internal draft writes cannot mutate the publication.
307
+ // record, so internal draft writes cannot mutate the publication.
309
308
  state = { ...next, stats: { ...next.stats } };
310
309
  },
311
310
  listeners,
@@ -413,7 +412,7 @@ export async function createLocalEngineSession({
413
412
  // discards the old transcript, so drop any tracked active tool calls.
414
413
  activeToolCalls.clear();
415
414
  const structureRevision = state.structureRevision;
416
- state = replaceEngineItemsState({
415
+ state = replaceSessionItemsState({
417
416
  state,
418
417
  items: liveItems,
419
418
  itemIndexById,
@@ -430,7 +429,7 @@ export async function createLocalEngineSession({
430
429
  ...transcriptHistoryFlags(),
431
430
  },
432
431
  });
433
- // replaceEngineItemsState retains its standalone/test contract. In the live
432
+ // replaceSessionItemsState retains its standalone/test contract. In the live
434
433
  // store, defer its revision increment to the frame publication boundary.
435
434
  state = { ...state, structureRevision };
436
435
  publisher.markStructureChange();
@@ -446,7 +445,7 @@ export async function createLocalEngineSession({
446
445
  // (App.jsx recentPromptHistory useMemo). It only changes when a user item is
447
446
  // appended, so rebuild it there and on bulk item swaps, publishing to
448
447
  // state.promptHistoryList. Pure derivation now lives in
449
- // ./engine/prompt-history.mjs (recomputePromptHistory); callers still pass the
448
+ // ./session/prompt-history.mjs (recomputePromptHistory); callers still pass the
450
449
  // NEW items array explicitly and publish via set().
451
450
  // --- Active-tool summary (Explore/Search) maintained incrementally ---
452
451
  // App previously scanned every transcript item on every change to derive the
@@ -576,7 +575,7 @@ export async function createLocalEngineSession({
576
575
  }
577
576
  return set({ streamingTail: null, ...extra });
578
577
  };
579
- const { patchItem, settleStreamingTail } = createEngineItemMutators({
578
+ const { patchItem, settleStreamingTail } = createSessionItemMutators({
580
579
  getState: () => state,
581
580
  set,
582
581
  itemIndexById,
@@ -823,7 +822,7 @@ export async function createLocalEngineSession({
823
822
 
824
823
 
825
824
  Object.assign(bag, {
826
- runtime, nextId, tuiDebug, LEAD_TURN_TIMEOUT_MS,
825
+ runtime, nextId, tuiDebug,
827
826
  flags, lifecycle, pending, pendingNotificationKeys, displayedExecutionNotificationKeys, clearExecutionDedupState, listeners, itemIndexById,
828
827
  getState: () => state, getPublishedState: () => publishedState,
829
828
  set, flushEmit, flushEmitImmediate, disposeEmit: publisher.dispose,
@@ -842,7 +841,7 @@ export async function createLocalEngineSession({
842
841
  });
843
842
  Object.assign(bag, createSessionFlow(bag));
844
843
  bag.runTurn = createRunTurn(bag);
845
- const api = createEngineApi(bag);
844
+ const api = createSessionApi(bag);
846
845
  // Cross-surface share: presence + the durable pending spool remain the
847
846
  // ownership/base layer; a local pipe layer (live-share) streams frame
848
847
  // deltas so co-open surfaces mirror each other in real time.
@@ -869,14 +868,10 @@ export async function createLocalEngineSession({
869
868
  const injected = (await runtime.takeRemoteInjections?.()) || [];
870
869
  if (injected.length === 0) return;
871
870
  for (const item of injected) {
872
- // New shape: { text, id }. Legacy drains still yield bare strings.
873
- if (item && typeof item === 'object' && (item.text != null || item.content != null)) {
874
- const text = item.text ?? item.content;
875
- const id = typeof item.id === 'string' && item.id.trim() ? item.id.trim() : undefined;
876
- bag.enqueue(text, id ? { id } : {});
877
- } else {
878
- bag.enqueue(item);
879
- }
871
+ if (!item || typeof item !== 'object' || (item.text == null && item.content == null)) continue;
872
+ const text = item.text ?? item.content;
873
+ const id = typeof item.id === 'string' && item.id.trim() ? item.id.trim() : undefined;
874
+ bag.enqueue(text, id ? { id } : {});
880
875
  }
881
876
  void bag.drain();
882
877
  })().catch(() => { /* the watch/tick pair retries */ });
@@ -895,7 +890,7 @@ export async function createLocalEngineSession({
895
890
  getPublishedState: () => publishedState,
896
891
  listeners,
897
892
  onRemoteSubmit: (text, meta) => {
898
- // A refusal must be REPORTED, never silent. This engine can be unable to
893
+ // A refusal must be REPORTED, never silent. This session runtime can be unable to
899
894
  // take a foreign prompt (disposed, or it became an attached viewer
900
895
  // itself); swallowing it here is what made a submitted message vanish
901
896
  // with no transcript row and no error (user: 입력이 씹힘). The ack sent
@@ -948,41 +943,46 @@ export async function createLocalEngineSession({
948
943
  // owner frames own stats/agent/tool state (see runtimePulseTimer above).
949
944
  bag.liveShareMirroring = () => state.sessionRemoteAttached && liveShare.viewerConnected();
950
945
  // Live viewer submits ride the owner's pipe (instant user bubble + shared
951
- // streaming); the durable spool path below remains the fallback.
946
+ // streaming); the durable spool remains the fallback. Returns null when this
947
+ // surface is NOT an attached viewer, so the local session runtime keeps the prompt.
948
+ const viewerSubmitIntake = (prompt, options = {}) => {
949
+ if (!state.sessionRemoteAttached) return null;
950
+ const text = String(promptDisplayText(prompt, options) || '').trim();
951
+ if (!text) return { accepted: false };
952
+ return {
953
+ accepted: forwardViewerSubmit({
954
+ text,
955
+ options,
956
+ share: liveShare,
957
+ // Writing to the owner's spool instead of starting a fake local turn
958
+ // that would render an error/synthetic assistant message here.
959
+ spool: (submissionId) => runtime.enqueueRemoteAttachedPrompt?.({
960
+ content: prompt,
961
+ text,
962
+ id: submissionId,
963
+ }) === true,
964
+ }),
965
+ };
966
+ };
952
967
  if (typeof api.submit === 'function') {
953
968
  const baseSubmit = api.submit;
954
969
  api.submit = (prompt, options = {}) => {
955
- if (state.sessionRemoteAttached) {
956
- const text = String(promptDisplayText(prompt, options) || '').trim();
957
- if (!text) return false;
958
- // Reconcile first so a session that became attachable this event-loop
959
- // turn uses the instant pipe path. If the pipe is still opening, write
960
- // directly to the durable owner spool instead of starting a fake local
961
- // turn that renders an error/synthetic assistant message.
962
- try { liveShare.ensure(); } catch { /* durable fallback below */ }
963
- const submissionId = options.id != null && String(options.id).trim()
964
- ? String(options.id).trim()
965
- : `view-submit-${process.pid}-${Date.now()}`;
966
- const spoolFallback = () => runtime.enqueueRemoteAttachedPrompt?.({
967
- content: prompt,
968
- text: text,
969
- id: submissionId,
970
- }) === true;
971
- // A pipe WRITE is not a delivery: the owner may refuse the prompt or
972
- // the socket may die between write and read. sendSubmit stays
973
- // synchronous (the store contract), but an unacknowledged submit is
974
- // re-delivered through the durable spool — late, never lost.
975
- if (text && liveShare.sendSubmit(text, {
976
- id: submissionId,
977
- submittedAt: options.submittedAt,
978
- onUndelivered: spoolFallback,
979
- })) return true;
980
- return spoolFallback();
981
- }
982
- return baseSubmit(prompt, options);
970
+ const forwarded = viewerSubmitIntake(prompt, options);
971
+ return forwarded ? forwarded.accepted : baseSubmit(prompt, options);
972
+ };
973
+ }
974
+ // The daemon's intake boundary is submitAsync (session-service prefers
975
+ // it over submit). Leaving it unwrapped let a daemon-hosted VIEWER — the
976
+ // desktop pane on a session the terminal owns — queue the prompt locally, so
977
+ // its own user row stood beside the owner's mirrored twin.
978
+ if (typeof api.submitAsync === 'function') {
979
+ const baseSubmitAsync = api.submitAsync;
980
+ api.submitAsync = async (prompt, options = {}) => {
981
+ const forwarded = viewerSubmitIntake(prompt, options);
982
+ return forwarded ? forwarded.accepted : baseSubmitAsync(prompt, options);
983
983
  };
984
984
  }
985
- // Viewer stop button: the local engine has no in-flight turn to cancel —
985
+ // Viewer stop button: the local session runtime has no in-flight turn to cancel —
986
986
  // forward the interrupt to the owner over the pipe. Falls back to the local
987
987
  // abort (no-op safe) when the pipe is down.
988
988
  if (typeof api.abort === 'function') {
@@ -1010,7 +1010,7 @@ export async function createLocalEngineSession({
1010
1010
  reconcileLiveShareNow();
1011
1011
  if (method === 'resume' && result === true && state.sessionRemoteAttached) {
1012
1012
  const id = String(state.sessionId || '');
1013
- // EngineHost holds renderer publications across resume. Keep that hold
1013
+ // The session projection holds renderer publications across resume. Keep that hold
1014
1014
  // until the owner's first FULL frame replaces the persisted transcript,
1015
1015
  // then synchronously publish the complete draft before getState().
1016
1016
  // A healthy local owner serves its full frame on connect within tens
@@ -1025,7 +1025,7 @@ export async function createLocalEngineSession({
1025
1025
  return result;
1026
1026
  };
1027
1027
  }
1028
- // Cover engines whose runtime already has a session at construction time;
1028
+ // Cover session runtimes whose runtime already has a session at construction time;
1029
1029
  // do not wait for a lifecycle method or the 3s safety pulse to open the pipe.
1030
1030
  reconcileLiveShareNow();
1031
1031
  // Instant input pickup: watch the shared pending spool so an attached
@@ -0,0 +1,22 @@
1
+ /**
2
+ * TUI session facade.
3
+ *
4
+ * Product clients receive only a daemon-owned session projection. The large
5
+ * in-process store is isolated in session-local.mjs and is not exposed
6
+ * from this client facade.
7
+ */
8
+ export {
9
+ cleanupStaleTranscriptSpillDirs,
10
+ createTranscriptSpillBuffer,
11
+ refillTranscriptViewOverlap,
12
+ replaceSessionItemsState,
13
+ createSessionItemMutators,
14
+ TRANSCRIPT_LIVE_ITEM_CAP,
15
+ TRANSCRIPT_SPILL_CHUNK_ITEMS,
16
+ } from './session/transcript-spill.mjs';
17
+ export { parseBackgroundTaskEnvelope } from './session/agent-envelope.mjs';
18
+
19
+ export async function createSessionRuntime(options = {}) {
20
+ const { createSession } = await import('../standalone/session-client.mjs');
21
+ return await createSession(options);
22
+ }
@@ -9,9 +9,6 @@
9
9
  * for 30s. One builder, one answer.
10
10
  */
11
11
 
12
- /** Short turns stay quiet: elapsed/token meta only after this long. */
13
- export const SHOW_TOKENS_AFTER_MS = 30_000;
14
-
15
12
  const compactNumberFormatter = new Intl.NumberFormat('en-US', {
16
13
  notation: 'compact',
17
14
  maximumFractionDigits: 1,
@@ -62,7 +59,10 @@ export function buildSpinnerMeta({
62
59
  const thinkingText = spinnerThinkingLabel({ thinking, thinkingSince, thinkingMs, effort });
63
60
  return {
64
61
  tokensText: tokens > 0 ? `${formatSpinnerTokens(tokens)} tokens` : '',
65
- showTokens: tokens > 0 && (Boolean(verbose) || Number(elapsedMs) > SHOW_TOKENS_AFTER_MS),
62
+ // The count appears with the FIRST token (user: 30초가 아니라 바로).
63
+ // Elapsed was never gated, so the byline used to grow a second field
64
+ // mid-turn for no reason the reader could see; now both arrive together.
65
+ showTokens: tokens > 0,
66
66
  thinkingText,
67
67
  thinkingActive: Boolean(thinking || Number(thinkingSince) > 0),
68
68
  };
@@ -48,6 +48,40 @@ export const SPINNER_VERBS = [
48
48
  /** How long one phrase holds before the pool advances. */
49
49
  export const SPINNER_VERB_ROTATE_MS = 30_000;
50
50
 
51
+ /** Odds that a 30s boundary actually swaps the phrase (user: 바꿀지 말지만
52
+ * 랜덤 돌리면 되잖아). Below 1 the window stretches: ~half the boundaries
53
+ * hold, so a phrase lives 30s, 60s, 90s… and the rhythm stops being a
54
+ * metronome. */
55
+ export const SPINNER_VERB_CHANGE_ODDS = 0.5;
56
+
57
+ /**
58
+ * Deterministic coin for ONE (turn, boundary) pair.
59
+ *
60
+ * Math.random() is not an option here: spinnerVerbFor runs on every render
61
+ * tick (~1/s) and the TUI and the desktop each call it for themselves, so a
62
+ * real RNG would reroll the word every second AND let the two surfaces
63
+ * disagree. An integer hash keeps each boundary's answer fixed forever while
64
+ * making the SEQUENCE of answers unpredictable — which is the part that reads
65
+ * as random.
66
+ */
67
+ function boundarySwaps(anchor, boundary) {
68
+ let h = (Math.floor(anchor / 1000) + Math.imul(boundary, 0x9e3779b1)) | 0;
69
+ h = Math.imul(h ^ (h >>> 16), 0x21f0aaad);
70
+ h = Math.imul(h ^ (h >>> 15), 0x735a2d97);
71
+ h = (h ^ (h >>> 15)) >>> 0;
72
+ return h / 0x100000000 < SPINNER_VERB_CHANGE_ODDS;
73
+ }
74
+
75
+ /** Past this many boundaries (~34h) every boundary simply swaps; the walk is
76
+ * re-counted each render and must not grow without bound. */
77
+ const MAX_COUNTED_BOUNDARIES = 4096;
78
+
79
+ /** A coin streak can hold a phrase for a very long time (measured: up to 9
80
+ * minutes over 300 turns). After this many consecutive holds the next
81
+ * boundary swaps regardless of its coin, capping one phrase at 4 windows
82
+ * (~2 min) while leaving the 30/60/90/120s variety intact. */
83
+ const MAX_HELD_BOUNDARIES = 3;
84
+
51
85
  /**
52
86
  * Modes that describe a STATE rather than ongoing work: they replace the pool
53
87
  * phrase for as long as the mode lasts, then the rotation resumes. Everything
@@ -64,14 +98,28 @@ export const SPINNER_MODE_OVERRIDE_VERBS = {
64
98
  /**
65
99
  * The phrase for a turn at a point in time. Derived from the turn's
66
100
  * `startedAt` so the TUI and the desktop show the SAME word at the same
67
- * second, and so mode flips cannot change it — only the 30s window can.
101
+ * second, and so mode flips cannot change it — only a 30s boundary can, and
102
+ * only when that boundary's coin says so.
68
103
  */
69
104
  export function spinnerVerbFor(startedAt = 0, now = 0, pool = SPINNER_VERBS) {
70
105
  const list = Array.isArray(pool) && pool.length ? pool : SPINNER_VERBS;
71
106
  const anchor = Math.max(0, Number(startedAt) || 0);
72
107
  const elapsed = anchor > 0 ? Math.max(0, (Number(now) || 0) - anchor) : 0;
73
- const slot = Math.floor(elapsed / SPINNER_VERB_ROTATE_MS);
74
- const seed = Math.floor(anchor / 1000) + slot;
108
+ const boundaries = Math.floor(elapsed / SPINNER_VERB_ROTATE_MS);
109
+ // Only the boundaries that FIRED advance the pool; a held boundary leaves
110
+ // the phrase exactly where it was, which is what stretches the window.
111
+ const counted = Math.min(boundaries, MAX_COUNTED_BOUNDARIES);
112
+ let swaps = boundaries - counted;
113
+ let held = 0;
114
+ for (let boundary = 1; boundary <= counted; boundary += 1) {
115
+ if (held >= MAX_HELD_BOUNDARIES || boundarySwaps(anchor, boundary)) {
116
+ swaps += 1;
117
+ held = 0;
118
+ } else {
119
+ held += 1;
120
+ }
121
+ }
122
+ const seed = Math.floor(anchor / 1000) + swaps;
75
123
  return list[((seed * 7) + 3) % list.length];
76
124
  }
77
125
 
@@ -54,7 +54,7 @@ export function shellJobsStatus({ clientHostPid, sessionId } = {}) {
54
54
  return (scope && value.sessions?.[scope]) || EMPTY_SHELL_JOBS_SESSION;
55
55
  }
56
56
 
57
- // Memory cycle L2 segment source. The memory daemon writes
57
+ // Memory cycle L2 segment source. The daemon-hosted memory runtime writes
58
58
  // data/memory-cycle-state.json on every cycle start/finish (index.mjs
59
59
  // _writeCycleStateFile) — no HTTP call from the statusline path. Cached at
60
60
  // the same 1s cadence as the shell segment. Shows a single unified "Memory"
@@ -351,7 +351,7 @@ function renderNativeStatusline({
351
351
  ], agentJobs);
352
352
  const { runningWorkers } = classifyAgentWorkers(agentPayload.workers);
353
353
  // Shell segment scope: one host process can own MANY sessions' jobs (the
354
- // desktop pools every pane's engine; the daemon backend hosts every
354
+ // desktop pools every pane's engine; the daemon hosts every
355
355
  // terminal's session), so the owner-wide aggregate would show one
356
356
  // terminal's running shell on every other terminal. Render this session's
357
357
  // own jobs whenever we know which session we are; only a session-less
@@ -42,7 +42,8 @@ function App({ children, stdin, stdout, stderr, writeToStdout, writeToStderr, ex
42
42
  const readableListenerRef = useRef(undefined);
43
43
  const inputParserRef = useRef(createInputParser());
44
44
  const pendingInputFlushRef = useRef(undefined);
45
- // Small delay to let chunked escape sequences complete before flushing as literal input.
45
+ // Only partial multi-byte escape sequences wait. A raw read containing one
46
+ // bare ESC is an explicit cancel gesture and must dispatch in this turn.
46
47
  const pendingInputFlushDelayMilliseconds = 20;
47
48
  const clearPendingInputFlush = useCallback(() => {
48
49
  if (!pendingInputFlushRef.current) {
@@ -233,10 +234,18 @@ function App({ children, stdin, stdout, stderr, writeToStdout, writeToStderr, ex
233
234
  let chunk;
234
235
  // eslint-disable-next-line @typescript-eslint/no-restricted-types
235
236
  while ((chunk = stdin.read()) !== null) {
237
+ const hadPendingEscape = inputParserRef.current.hasPendingEscape();
238
+ const bareEscapeChunk = !hadPendingEscape && String(chunk) === escape;
236
239
  const inputEvents = inputParserRef.current.push(chunk);
237
240
  for (const event of inputEvents) {
238
241
  dispatchParsedEvent(event);
239
242
  }
243
+ if (bareEscapeChunk && inputParserRef.current.hasPendingEscape()) {
244
+ const escapeEvents = inputParserRef.current.flushPendingEscape();
245
+ for (const event of escapeEvents ?? []) {
246
+ dispatchParsedEvent(event);
247
+ }
248
+ }
240
249
  }
241
250
  if (inputParserRef.current.hasPendingEscape()) {
242
251
  schedulePendingInputFlush();
@@ -1,61 +0,0 @@
1
- // Eager memory-daemon boot for remote sessions. The channels worker forwards
2
- // transcript ingests to the memory HTTP service, so its port must be published
3
- // (and actually listening) before the worker sends its first ingest — otherwise
4
- // early channel traffic finds no port and gets buffered. Extracted from
5
- // runtime-core's startRemote, which now just fires it.
6
- import * as httpMod from 'node:http';
7
-
8
- const HEALTH_ATTEMPTS = 30;
9
- const HEALTH_RETRY_MS = 500;
10
- const HEALTH_TIMEOUT_MS = 1500;
11
-
12
- function healthOk(port) {
13
- return new Promise((resolve) => {
14
- const request = httpMod.request(
15
- { hostname: '127.0.0.1', port, path: '/health', timeout: HEALTH_TIMEOUT_MS },
16
- (response) => {
17
- let body = '';
18
- response.on('data', (chunk) => { body += chunk; });
19
- response.on('end', () => {
20
- try { resolve(JSON.parse(body)?.status === 'ok'); } catch { resolve(false); }
21
- });
22
- },
23
- );
24
- request.on('error', () => resolve(false));
25
- request.on('timeout', () => { request.destroy(); resolve(false); });
26
- request.end();
27
- });
28
- }
29
-
30
- /** Detached, never throws: resolving the module only hands back a handle, so
31
- * this awaits start() and then polls /health until the daemon is provably
32
- * reachable (or the failure is profiled). */
33
- export function startMemoryDaemonEagerly({ getMemoryModule, bootProfile }) {
34
- void (async () => {
35
- try {
36
- // Yield one tick first: the module resolve + daemon fork below is a long
37
- // synchronous chain that would otherwise starve Ink's queued render and
38
- // keypress handling.
39
- await new Promise((resolve) => setImmediate(resolve));
40
- const module = await getMemoryModule();
41
- const started = typeof module?.start === 'function' ? await module.start() : null;
42
- const port = started?.port;
43
- if (!port) {
44
- bootProfile('channels:memory-eager-init-failed', { error: 'no port from start()' });
45
- return;
46
- }
47
- for (let attempt = 0; attempt < HEALTH_ATTEMPTS; attempt += 1) {
48
- try {
49
- if (await healthOk(port)) {
50
- bootProfile('channels:memory-eager-init-ready', { port });
51
- return;
52
- }
53
- } catch { /* probe failure is retried below */ }
54
- await new Promise((resolve) => setTimeout(resolve, HEALTH_RETRY_MS));
55
- }
56
- bootProfile('channels:memory-eager-init-failed', { error: `health not ok after retries (port ${port})` });
57
- } catch (error) {
58
- bootProfile('channels:memory-eager-init-failed', { error: error?.message || String(error) });
59
- }
60
- })();
61
- }