mixdog 0.9.96 → 0.9.99

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 (193) hide show
  1. package/README.md +0 -17
  2. package/package.json +16 -8
  3. package/scripts/build-tui.mjs +6 -6
  4. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  5. package/src/defaults/skills/setup/SKILL.md +1 -1
  6. package/src/lib/keychain-cjs.cjs +1 -1
  7. package/src/lib/mixdog-debug.cjs +2 -3
  8. package/src/rules/agent/30-explorer.md +43 -32
  9. package/src/rules/lead/01-general.md +1 -2
  10. package/src/rules/lead/lead-tool.md +0 -3
  11. package/src/rules/shared/01-tool.md +11 -10
  12. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  13. package/src/runtime/agent/orchestrator/agent-trace.mjs +19 -5
  14. package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
  15. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +7 -0
  16. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -3
  17. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +5 -3
  18. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  19. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +23 -1
  20. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  21. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
  22. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  23. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
  24. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
  25. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
  26. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +42 -26
  27. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  28. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +44 -6
  29. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  30. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -1
  31. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  32. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +3 -6
  33. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  34. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  35. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
  36. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
  37. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
  38. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  39. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
  41. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +103 -12
  42. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
  43. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
  44. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +10 -30
  45. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
  46. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
  47. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
  48. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  49. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
  50. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
  51. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  52. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
  53. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -105
  55. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
  56. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  57. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
  58. package/src/runtime/attachments/pdf-extract.mjs +83 -0
  59. package/src/runtime/attachments/store.mjs +515 -0
  60. package/src/runtime/attachments/store.test.mjs +188 -0
  61. package/src/runtime/channels/lib/config.mjs +33 -33
  62. package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
  63. package/src/runtime/channels/lib/interaction-handlers.mjs +4 -194
  64. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  65. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  66. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  67. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  68. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  69. package/src/runtime/channels/lib/runtime-paths.mjs +5 -34
  70. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  71. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  72. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  73. package/src/runtime/channels/lib/worker-ipc.mjs +4 -70
  74. package/src/runtime/channels/lib/worker-main.mjs +39 -58
  75. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  76. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  77. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  78. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  79. package/src/runtime/channels/tool-defs.mjs +1 -1
  80. package/src/runtime/memory/index.mjs +72 -211
  81. package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
  82. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
  83. package/src/runtime/memory/lib/http-router.mjs +1 -6
  84. package/src/runtime/memory/lib/http-wire.mjs +39 -4
  85. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  86. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  87. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  88. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  89. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  90. package/src/runtime/memory/lib/memory.mjs +1 -1
  91. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  92. package/src/runtime/search/index.mjs +2 -2
  93. package/src/runtime/shared/abort-race.mjs +70 -0
  94. package/src/runtime/shared/child-spawn-gate.mjs +101 -63
  95. package/src/runtime/shared/config.mjs +11 -24
  96. package/src/runtime/shared/json-metrics.mjs +94 -0
  97. package/src/runtime/shared/llm/index.mjs +1 -1
  98. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  99. package/src/runtime/shared/resource-admission.mjs +6 -5
  100. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  101. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  102. package/src/runtime/shared/tool-surface.mjs +91 -22
  103. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  104. package/src/runtime/shared/turn-snapshot.mjs +30 -2
  105. package/src/runtime/shared/turn-worktree-snapshot.mjs +19 -0
  106. package/src/session-runtime/channel-config-api.mjs +5 -5
  107. package/src/session-runtime/config-lifecycle.mjs +27 -27
  108. package/src/session-runtime/lifecycle-api.mjs +22 -3
  109. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  110. package/src/session-runtime/prewarm.mjs +1 -1
  111. package/src/session-runtime/provider-models.mjs +12 -2
  112. package/src/session-runtime/quick-model-rows.mjs +38 -7
  113. package/src/session-runtime/quick-search-models.mjs +15 -17
  114. package/src/session-runtime/remote-control.mjs +6 -7
  115. package/src/session-runtime/runtime-core.mjs +61 -32
  116. package/src/session-runtime/runtime-tunables.mjs +9 -6
  117. package/src/session-runtime/self-update.mjs +1 -1
  118. package/src/session-runtime/session-lifecycle.mjs +36 -15
  119. package/src/session-runtime/session-title.mjs +28 -4
  120. package/src/session-runtime/session-turn-api.mjs +55 -15
  121. package/src/session-runtime/settings-api.mjs +7 -7
  122. package/src/standalone/agent-dispatch-broker.mjs +1 -1
  123. package/src/standalone/channel-admin.mjs +42 -53
  124. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
  125. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +73 -58
  126. package/src/standalone/channel-worker.mjs +124 -647
  127. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +190 -132
  128. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  129. package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
  130. package/src/standalone/session-protocol.mjs +9 -4
  131. package/src/standalone/session-runtime-pool.mjs +393 -0
  132. package/src/standalone/session-runtime-record.mjs +20 -0
  133. package/src/standalone/session-runtime-worker.mjs +203 -0
  134. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +192 -157
  135. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +142 -63
  136. package/src/standalone/session-wire.mjs +55 -0
  137. package/src/tui/App.jsx +4 -4
  138. package/src/tui/app/app-format.mjs +1 -1
  139. package/src/tui/app/channel-pickers.mjs +41 -41
  140. package/src/tui/app/input-parsers.mjs +2 -2
  141. package/src/tui/app/onboarding-steps.mjs +2 -2
  142. package/src/tui/app/project-picker.mjs +8 -8
  143. package/src/tui/app/prompt-submit.mjs +15 -17
  144. package/src/tui/app/settings-picker.mjs +25 -25
  145. package/src/tui/app/use-prompt-handlers.mjs +7 -3
  146. package/src/tui/app/use-prompt-queue-history.mjs +1 -1
  147. package/src/tui/app/use-transcript-scroll.mjs +1 -1
  148. package/src/tui/components/ToolExecution.jsx +1 -1
  149. package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
  150. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  151. package/src/tui/dist/index.mjs +403 -170
  152. package/src/tui/hooks/useSession.mjs +16 -0
  153. package/src/tui/index.jsx +11 -11
  154. package/src/tui/paste-attachments.mjs +13 -7
  155. package/src/tui/prompt-history-store.mjs +52 -4
  156. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  157. package/src/tui/{engine → session}/agent-job-feed.mjs +9 -5
  158. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  159. package/src/tui/{engine → session}/context-state.mjs +3 -3
  160. package/src/tui/{engine → session}/labels.mjs +2 -2
  161. package/src/tui/{engine → session}/live-share.mjs +28 -14
  162. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  163. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  164. package/src/tui/session/oauth-flows.mjs +140 -0
  165. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  166. package/src/tui/{engine → session}/queue-helpers.mjs +8 -8
  167. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  168. package/src/tui/{engine → session}/session-api-ext.mjs +42 -21
  169. package/src/tui/{engine → session}/session-api.mjs +17 -61
  170. package/src/tui/{engine → session}/session-flow.mjs +5 -3
  171. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  172. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  173. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  174. package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
  175. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  176. package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
  177. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  178. package/src/tui/{engine → session}/turn.mjs +29 -107
  179. package/src/tui/{engine-local-session.mjs → session-local.mjs} +72 -78
  180. package/src/tui/session.mjs +22 -0
  181. package/src/ui/statusline-segments.mjs +1 -1
  182. package/src/ui/statusline.mjs +1 -1
  183. package/vendor/ink/build/components/App.js +10 -1
  184. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  185. package/src/standalone/engine-daemon-protocol.mjs +0 -32
  186. package/src/tui/engine/turn-watchdog.mjs +0 -92
  187. package/src/tui/engine.mjs +0 -22
  188. package/src/tui/hooks/useEngine.mjs +0 -16
  189. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  190. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  191. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  192. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  193. /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
@@ -1,14 +1,13 @@
1
- // Engine pool hosted by the machine-global engine daemon.
1
+ // Session runtime pool hosted by the machine-global daemon.
2
2
  //
3
- // One process owns every live session engine; the terminal TUI and the desktop
3
+ // One process owns every live session runtime; the terminal TUI and the desktop
4
4
  // app attach as VIEWS over the transport. That inverts today's model (each
5
- // client boots its own engine and the session store arbitrates ownership with
5
+ // client boots its own session runtime and the session store arbitrates ownership with
6
6
  // generation counters + heartbeat vetoes): with a single writer, cross-client
7
7
  // editing is just fan-out, and the split-brain guards can never trip against
8
8
  // our own second client.
9
9
  //
10
- // The engine factory is injected (`createEngine`) so the daemon entry supplies
11
- // the real createEngineSession while tests supply a stub.
10
+ // The session runtime factory is injected by the daemon entry.
12
11
  import { randomUUID } from 'node:crypto';
13
12
  import { createRequire } from 'node:module';
14
13
  import { fileURLToPath } from 'node:url';
@@ -16,11 +15,15 @@ import {
16
15
  SESSION_CONFIGURE_ACTION_SET,
17
16
  SESSION_READ_ACTION_SET,
18
17
  } from './session-protocol.mjs';
18
+ import {
19
+ materializePromptSubmission,
20
+ preparePromptSubmissionForProvider,
21
+ } from '../runtime/attachments/store.mjs';
19
22
 
20
23
  const MAX_CLONE_DEPTH = 24;
21
- const requireDesktopBackend = createRequire(import.meta.url);
24
+ const requireDesktopService = createRequire(import.meta.url);
22
25
 
23
- async function loadDesktopBackendModule(moduleUrl) {
26
+ async function loadDesktopServiceModule(moduleUrl) {
24
27
  const parsed = new URL(moduleUrl);
25
28
  if (!parsed.pathname.toLowerCase().endsWith('.cjs')) {
26
29
  return import(moduleUrl);
@@ -30,12 +33,12 @@ async function loadDesktopBackendModule(moduleUrl) {
30
33
  // already-instantiated adapters alive, but load this newly keyed artifact
31
34
  // from disk for the new desktop build.
32
35
  const modulePath = fileURLToPath(parsed);
33
- const resolved = requireDesktopBackend.resolve(modulePath);
34
- delete requireDesktopBackend.cache[resolved];
35
- return requireDesktopBackend(resolved);
36
+ const resolved = requireDesktopService.resolve(modulePath);
37
+ delete requireDesktopService.cache[resolved];
38
+ return requireDesktopService(resolved);
36
39
  }
37
40
 
38
- /** JSON-safe projection of an engine snapshot. Functions, symbols, and
41
+ /** JSON-safe projection of a session snapshot. Functions, symbols, and
39
42
  * undefined never survive a transport hop; dropping them here (instead of at
40
43
  * JSON.stringify time) keeps object identity stable for the receiver and
41
44
  * makes cycles impossible rather than fatal. */
@@ -88,8 +91,8 @@ export function sanitizeForWire(value, depth = 0, seen = new WeakSet()) {
88
91
  }
89
92
  }
90
93
 
91
- export function createEngineDaemonService({
92
- createEngine,
94
+ export function createSessionService({
95
+ createSessionRuntime = null,
93
96
  sessionExists = null,
94
97
  listSessions = null,
95
98
  desktopRuntime = null,
@@ -100,14 +103,13 @@ export function createEngineDaemonService({
100
103
  idleEvictMs = null,
101
104
  evictSweepMs = null,
102
105
  } = {}) {
103
- if (typeof createEngine !== 'function') throw new Error('createEngine is required');
106
+ const createRuntime = createSessionRuntime;
107
+ if (typeof createRuntime !== 'function') throw new Error('createSessionRuntime is required');
104
108
  // Revisions optimize deltas inside one daemon lifetime; sessionId + full
105
109
  // snapshots remain the durable contract. Start each daemon far above the
106
- // prior wall-clock epoch so an already-running legacy view (which compared
107
- // revisions across reconnects) still accepts the replacement daemon's first
108
- // full snapshot. New views explicitly reset their revision baseline on
109
- // attachment replacement and do not depend on this compatibility seed.
110
- const configuredRevisionEpoch = Number(process.env.MIXDOG_ENGINE_REVISION_EPOCH);
110
+ // prior wall-clock epoch so a view reconnecting after replacement accepts
111
+ // the new daemon's first full snapshot.
112
+ const configuredRevisionEpoch = Number(process.env.MIXDOG_SESSION_REVISION_EPOCH);
111
113
  const revisionEpoch = Number.isSafeInteger(configuredRevisionEpoch)
112
114
  && configuredRevisionEpoch >= 0
113
115
  ? configuredRevisionEpoch
@@ -118,32 +120,29 @@ export function createEngineDaemonService({
118
120
  const sessions = new Set();
119
121
  const sessionsById = new Map();
120
122
  let busyEntries = 0;
121
- // Desktop builds can overlap during an update (or while a dev preview is
122
- // open). Their adapters are build artifacts and therefore must not be
123
- // mistaken for one process-global implementation. Reuse an adapter only for
124
- // the same module URL; compatible builds otherwise coexist on the stable
125
- // daemon wire protocol.
126
- const desktopBackendsById = new Map();
127
- const desktopBackendsByModule = new Map();
128
- const desktopBackendPromises = new Map();
123
+ // Desktop adapters are keyed by their exact module URL so a dev preview and
124
+ // installed build never share module state accidentally.
125
+ const desktopServicesById = new Map();
126
+ const desktopServicesByModule = new Map();
127
+ const desktopServicePromises = new Map();
129
128
  let projectStorePromise = null;
130
129
  let closed = false;
131
130
  // A turn belongs to the DAEMON, not to whoever is watching it: closing the
132
- // desktop window or restarting the TUI must never interrupt work. An engine
131
+ // desktop window or restarting the TUI must never interrupt work. An session runtime
133
132
  // whose last view left is RETAINED while it is busy and evicted only after it
134
133
  // has been idle and unwatched for this long. With a view release no longer
135
134
  // destroying anything, this sweep is the ONLY reclaim path besides shutdown.
136
135
  const IDLE_EVICT_MS = Number(idleEvictMs) > 0
137
136
  ? Number(idleEvictMs)
138
- : Math.max(60_000, Number(process.env.MIXDOG_ENGINE_IDLE_EVICT_MS) || 2 * 60_000);
137
+ : Math.max(60_000, Number(process.env.MIXDOG_SESSION_IDLE_EVICT_MS) || 2 * 60_000);
139
138
  const EVICT_SWEEP_MS = Number(evictSweepMs) > 0 ? Number(evictSweepMs) : 30_000;
140
139
  let evictTimer = null;
141
140
 
142
141
  // ── Cross-client subscriptions ──────────────────────────────────────────────
143
- // An engine is shared by construction (terminal + desktop converge on one
144
- // engine per session), but each client process only refcounts the mirrors it
142
+ // An session runtime is shared by construction (terminal + desktop converge on one
143
+ // session runtime per session), but each client process only refcounts the mirrors it
145
144
  // holds ITSELF. Without a daemon-side viewer set, the first client to quit
146
- // destroyed an engine the other one was still streaming — the turn cut out
145
+ // destroyed an session runtime the other one was still streaming — the turn cut out
147
146
  // mid-answer and the surviving view stalled. Viewers are keyed by daemon
148
147
  // CLIENT token, so "the last view left" is a machine-wide fact.
149
148
  function subscriberToken(ctx) {
@@ -160,15 +159,15 @@ export function createEngineDaemonService({
160
159
  }
161
160
 
162
161
  /** A client that deregistered (or whose process died) stops being a viewer.
163
- * Its engines are never destroyed here: work outlives the client that
164
- * walked away, so an engine nobody watches goes back on the idle clock. */
162
+ * Its session runtimes are never destroyed here: work outlives the client that
163
+ * walked away, so an session runtime nobody watches goes back on the idle clock. */
165
164
  function releaseClient(clientToken) {
166
165
  const token = String(clientToken || '');
167
166
  if (!token) return { ok: true };
168
167
  for (const entry of sessions) {
169
168
  if (!entry.subscribers?.delete(token)) continue;
170
169
  if (entry.subscribers.size > 0) continue;
171
- if (entry.reservedOnly && !engineBusy(entry)) {
170
+ if (entry.reservedOnly && !sessionBusy(entry)) {
172
171
  void destroy(entry, 'unclaimed session reservation', {
173
172
  keepBackgroundWork: true,
174
173
  });
@@ -178,8 +177,8 @@ export function createEngineDaemonService({
178
177
  startEvictionSweep();
179
178
  log(`session ${currentSessionId(entry) || '(creating)'} unwatched (client ${token} gone) — retained`);
180
179
  }
181
- for (const backend of desktopBackendsById.values()) {
182
- backend.subscribers.delete(token);
180
+ for (const service of desktopServicesById.values()) {
181
+ service.subscribers.delete(token);
183
182
  }
184
183
  return { ok: true };
185
184
  }
@@ -198,12 +197,12 @@ export function createEngineDaemonService({
198
197
  return next;
199
198
  }
200
199
 
201
- function engineBusy(entry) {
200
+ function sessionBusy(entry) {
202
201
  if (typeof entry?.busy === 'boolean') return entry.busy;
203
202
  try {
204
- return updateEntryBusy(entry, entry.engine.getState?.() || {});
203
+ return updateEntryBusy(entry, entry.runtime.getState?.() || {});
205
204
  } catch {
206
- // An engine we cannot read is never assumed idle — losing a live turn is
205
+ // An session runtime we cannot read is never assumed idle — losing a live turn is
207
206
  // far worse than holding an extra process for one sweep.
208
207
  return true;
209
208
  }
@@ -215,16 +214,26 @@ export function createEngineDaemonService({
215
214
  const now = Date.now();
216
215
  for (const entry of [...sessions]) {
217
216
  if (!entry.retainedAt) continue;
218
- // A client came back to it: watched engines are never reclaimed.
217
+ // A client came back to it: watched session runtimes are never reclaimed.
219
218
  if (entry.subscribers?.size > 0) { entry.retainedAt = null; continue; }
220
- if (engineBusy(entry)) { entry.retainedAt = now; continue; }
219
+ if (sessionBusy(entry)) { entry.retainedAt = now; continue; }
221
220
  if (now - entry.retainedAt < IDLE_EVICT_MS) continue;
222
221
  void destroy(entry, 'idle and unwatched');
223
222
  }
223
+ stopEvictionSweepIfIdle();
224
224
  }, EVICT_SWEEP_MS);
225
225
  evictTimer.unref?.();
226
226
  }
227
227
 
228
+ function stopEvictionSweepIfIdle() {
229
+ if (!evictTimer) return;
230
+ for (const entry of sessions) {
231
+ if (!entry.disposed && entry.retainedAt && (entry.subscribers?.size || 0) === 0) return;
232
+ }
233
+ clearInterval(evictTimer);
234
+ evictTimer = null;
235
+ }
236
+
228
237
  function retainUnwatched(entry, reason = 'headless session budget') {
229
238
  if (!entry || entry.disposed || (entry.subscribers?.size || 0) > 0) return;
230
239
  entry.headless = true;
@@ -246,7 +255,7 @@ export function createEngineDaemonService({
246
255
  }
247
256
 
248
257
  function snapshotOf(entry) {
249
- const raw = entry.engine.getState?.() ?? null;
258
+ const raw = entry.runtime.getState?.() ?? null;
250
259
  // Store states are immutable snapshots (every mutation makes a new object),
251
260
  // so identity is a sound cache key. Without this the whole transcript was
252
261
  // re-sanitized on every call AND every published frame.
@@ -267,6 +276,20 @@ export function createEngineDaemonService({
267
276
  const items = entry.itemCache ??= new Map();
268
277
  const out = {};
269
278
  for (const [key, value] of Object.entries(raw)) {
279
+ if (key === 'queued' && Array.isArray(value)) {
280
+ const projected = value.map((item) => ({
281
+ id: item?.id,
282
+ submittedAt: item?.submittedAt,
283
+ text: String(item?.displayText ?? item?.text ?? '').slice(0, 2_000),
284
+ displayText: String(item?.displayText ?? item?.text ?? '').slice(0, 2_000),
285
+ mode: item?.mode || 'prompt',
286
+ priority: item?.priority || 'next',
287
+ ...(Array.isArray(item?.images) && item.images.length ? { images: item.images } : {}),
288
+ }));
289
+ fields.set(key, { source: value, value: projected });
290
+ out[key] = projected;
291
+ continue;
292
+ }
270
293
  if (key === 'items' && Array.isArray(value)) {
271
294
  const nextItems = new Map();
272
295
  out.items = value.map((item) => {
@@ -315,7 +338,7 @@ export function createEngineDaemonService({
315
338
  return { set, remove, itemsAppend };
316
339
  }
317
340
 
318
- /** Advance the engine's published revision one step. */
341
+ /** Advance the session runtime's published revision one step. */
319
342
  function advance(entry) {
320
343
  const snapshot = snapshotOf(entry);
321
344
  indexSessionEntry(entry, snapshot?.sessionId);
@@ -345,7 +368,7 @@ export function createEngineDaemonService({
345
368
  }
346
369
 
347
370
  function currentSessionId(entry) {
348
- return String(entry?.engine?.getState?.()?.sessionId || '');
371
+ return String(entry?.runtime?.getState?.()?.sessionId || '');
349
372
  }
350
373
 
351
374
  function indexSessionEntry(entry, sessionId = currentSessionId(entry)) {
@@ -360,27 +383,23 @@ export function createEngineDaemonService({
360
383
  }
361
384
  const existing = sessionsById.get(nextId);
362
385
  if (existing && existing !== entry && !existing.disposed) {
363
- // Do not redirect an established address to a second engine. The load
364
- // coordinator normally makes this impossible; retaining the first owner
365
- // is the safest fallback if an embedder mutates a session id directly.
366
- entry.indexedSessionId = '';
367
- log(`duplicate session address refused session=${nextId}`);
368
- return nextId;
386
+ // Do not redirect an established address to a second session runtime. The load
387
+ throw new Error(`duplicate session address: ${nextId}`);
369
388
  }
370
389
  sessionsById.set(nextId, entry);
371
390
  entry.indexedSessionId = nextId;
372
391
  return nextId;
373
392
  }
374
393
 
375
- /** Publish one durable session-addressed frame. The engine pool is a daemon
394
+ /** Publish one durable session-addressed frame. The runtime pool is a daemon
376
395
  * implementation detail and never enters the client contract. */
377
396
  function publishStep(entry, step) {
378
397
  const sessionId = currentSessionId(entry);
379
398
  if (!sessionId) return;
380
- // Engine revisions may predate the session address (a reservation becomes
399
+ // Session runtime revisions may predate the session address (a reservation becomes
381
400
  // a materialized session during newSession/resume). A session subscriber
382
- // has no copy of that engine-only base, so the first frame for each session
383
- // address must be FULL; only later frames may use engine revision deltas.
401
+ // has no copy of that session runtime-only base, so the first frame for each session
402
+ // address must be FULL; only later frames may use session runtime revision deltas.
384
403
  const body = entry.publishedSessionId === sessionId
385
404
  ? frameBody(step)
386
405
  : { revision: step.revision, full: step.snapshot };
@@ -420,7 +439,7 @@ export function createEngineDaemonService({
420
439
  // A headless turn still needs busy/index liveness, but no client can
421
440
  // consume a wire projection. Avoid cloning the growing transcript on
422
441
  // every token; the next subscriber receives a fresh full snapshot.
423
- const raw = entry.engine.getState?.() || {};
442
+ const raw = entry.runtime.getState?.() || {};
424
443
  indexSessionEntry(entry, raw.sessionId);
425
444
  updateEntryBusy(entry, raw);
426
445
  releaseProjection(entry);
@@ -436,7 +455,7 @@ export function createEngineDaemonService({
436
455
  }
437
456
  }
438
457
 
439
- /** Engine events fire per streamed token. Publish immediately after an idle
458
+ /** Session runtime events fire per streamed token. Publish immediately after an idle
440
459
  * interval, then coalesce the rest of the burst to one display-frame clock.
441
460
  * This avoids charging every first token a fixed delay. */
442
461
  function schedulePublish(entry) {
@@ -450,8 +469,9 @@ export function createEngineDaemonService({
450
469
  }
451
470
 
452
471
  async function createEntry(params = {}, ctx = null) {
453
- if (closed) throw new Error('engine daemon service is closed');
454
- const engine = await createEngine({
472
+ if (closed) throw new Error('session service is closed');
473
+ const runtime = await createRuntime({
474
+ sessionId: params.sessionId,
455
475
  cwd: params.cwd || process.cwd(),
456
476
  provider: params.provider,
457
477
  model: params.model,
@@ -460,14 +480,14 @@ export function createEngineDaemonService({
460
480
  desktopSession: params.desktopSession ?? null,
461
481
  });
462
482
  const entry = {
463
- engine, cwd: params.cwd || process.cwd(), timer: null, disposed: false,
483
+ runtime, cwd: params.cwd || process.cwd(), timer: null, disposed: false,
464
484
  unsubscribe: null, subscribers: new Set(), reservedOnly: false, lastPublishedAt: 0,
465
485
  indexedSessionId: '', busy: null, headless: !subscriberToken(ctx), retainedAt: null,
466
486
  revision: revisionEpoch,
467
487
  };
468
488
  sessions.add(entry);
469
489
  try {
470
- const initialState = engine.getState?.() || {};
490
+ const initialState = runtime.getState?.() || {};
471
491
  indexSessionEntry(entry, initialState.sessionId);
472
492
  updateEntryBusy(entry, initialState);
473
493
  } catch {
@@ -475,7 +495,7 @@ export function createEngineDaemonService({
475
495
  }
476
496
  addSubscriber(entry, ctx);
477
497
  try {
478
- entry.unsubscribe = engine.subscribe?.(() => schedulePublish(entry)) ?? null;
498
+ entry.unsubscribe = runtime.subscribe?.(() => schedulePublish(entry)) ?? null;
479
499
  } catch (err) {
480
500
  log(`session subscribe failed: ${err?.message || err}`);
481
501
  }
@@ -484,31 +504,32 @@ export function createEngineDaemonService({
484
504
 
485
505
  // ── Session-addressed calls ─────────────────────────────────────────────────
486
506
  // Views are RENDERERS: a desktop pane (or a TUI tab) must be able to hand the
487
- // backend a prompt for any session it can see, without owning an engine for
488
- // it first. The daemon resolves the session to its engine — LOADING one when
507
+ // service a prompt for any session it can see, without owning an session runtime for
508
+ // it first. The daemon resolves the session to its session runtime — LOADING one when
489
509
  // nothing hosts it — so "that session is not live here" can never reject user
490
510
  // input (user: 채팅이 안 쳐짐).
491
511
  const sessionLoads = new Map(); // sessionId -> Promise<entry>
492
512
 
493
- async function loadSessionEngine(sessionId, hints) {
513
+ async function loadSessionRuntime(sessionId, hints) {
494
514
  const entry = await createEntry({
515
+ sessionId,
495
516
  cwd: hints.cwd,
496
517
  provider: hints.provider,
497
518
  model: hints.model,
498
519
  toolMode: hints.toolMode,
499
520
  desktopSession: hints.desktopSession ?? null,
500
521
  });
501
- // Nothing is watching this engine yet: it is the daemon's own load, so the
522
+ // Nothing is watching this session runtime yet: it is the daemon's own load, so the
502
523
  // idle sweep must be able to reclaim it if no view ever attaches.
503
524
  entry.headless = true;
504
525
  let resumed = false;
505
526
  try {
506
- resumed = await entry.engine.resume?.(sessionId, hints.resumeOptions || undefined) === true;
527
+ resumed = await entry.runtime.resume?.(sessionId, hints.resumeOptions || undefined) === true;
507
528
  } catch (err) {
508
529
  await destroy(entry, 'session load failed');
509
530
  throw err;
510
531
  }
511
- const state = entry.engine.getState?.() || {};
532
+ const state = entry.runtime.getState?.() || {};
512
533
  const loaded = String(state.sessionId || '');
513
534
  // Fork-on-resume names its origin; any other id is a failed load.
514
535
  const forkedFrom = String(state.sessionForkedFrom || '');
@@ -518,15 +539,15 @@ export function createEngineDaemonService({
518
539
  }
519
540
  // Publish/index the resumed identity before sessionLoads releases its
520
541
  // single-flight promise. A second pane arriving in the next microtask must
521
- // find this owner in O(1), not create a duplicate engine.
542
+ // find this owner in O(1), not create a duplicate session runtime.
522
543
  advance(entry);
523
544
  retainUnwatched(entry, 'headless session load');
524
545
  log(`session ${sessionId} loaded on demand`);
525
546
  return entry;
526
547
  }
527
548
 
528
- /** The engine hosting sessionId: the existing owner, or a fresh load. One
529
- * load per session at a time — concurrent panes converge on one engine. */
549
+ /** The runtime hosting sessionId: the existing owner, or a fresh load. One
550
+ * load per session at a time — concurrent panes converge on one session runtime. */
530
551
  async function entryForSession(sessionId, hints = {}) {
531
552
  const owner = sessionOwner(sessionId);
532
553
  if (owner) return owner;
@@ -543,7 +564,7 @@ export function createEngineDaemonService({
543
564
  if (lateOwner) return lateOwner;
544
565
  throw new Error(`session ${sessionId} is not available`);
545
566
  }
546
- return loadSessionEngine(sessionId, hints);
567
+ return loadSessionRuntime(sessionId, hints);
547
568
  })().finally(() => {
548
569
  if (sessionLoads.get(sessionId) === loading) sessionLoads.delete(sessionId);
549
570
  });
@@ -554,14 +575,14 @@ export function createEngineDaemonService({
554
575
  async function runSessionAction({
555
576
  sessionId, action, args = [], open: openHints = {}, baseRevision = null,
556
577
  } = {}, allowedActions) {
557
- if (closed) throw new Error('engine daemon service is closed');
578
+ if (closed) throw new Error('session service is closed');
558
579
  const id = String(sessionId || '');
559
580
  if (!id) throw new TypeError('sessionId is required');
560
581
  const name = requireSessionAction(action, allowedActions);
561
582
  const entry = await entryForSession(id, openHints || {});
562
- const target = entry.engine[name];
583
+ const target = entry.runtime[name];
563
584
  if (typeof target !== 'function') throw new TypeError(`session action ${name} is unavailable`);
564
- const value = await target.apply(entry.engine, Array.isArray(args) ? args : []);
585
+ const value = await target.apply(entry.runtime, Array.isArray(args) ? args : []);
565
586
  if (name === 'setCwd' && value) {
566
587
  try {
567
588
  const projects = await loadProjectStore();
@@ -570,7 +591,7 @@ export function createEngineDaemonService({
570
591
  log(`project recency update failed (non-fatal): ${error?.message || error}`);
571
592
  }
572
593
  }
573
- // Keep one compact record that the action reached the backend without
594
+ // Keep one compact record that the action reached the service without
574
595
  // serializing transcripts/catalogs into the daemon log.
575
596
  const valueSummary = value === null || value === undefined
576
597
  ? String(value)
@@ -584,18 +605,18 @@ export function createEngineDaemonService({
584
605
  if (step.changed) {
585
606
  publishStep(entry, step);
586
607
  }
587
- // Still unwatched: keep it on the retention clock exactly like an engine a
588
- // view released, so an untouched load cannot leak past the idle window.
608
+ // Still unwatched: keep it on the retention clock exactly like a session runtime
609
+ // released by its view, so an untouched load cannot leak past the idle window.
589
610
  retainUnwatched(entry);
590
611
  return {
591
612
  value: sanitizeForWire(value) ?? null,
592
- sessionId: String(entry.engine.getState?.()?.sessionId || id),
613
+ sessionId: String(entry.runtime.getState?.()?.sessionId || id),
593
614
  ...bodyForClient(step, Number.isInteger(baseRevision) ? baseRevision : null),
594
615
  };
595
616
  }
596
617
 
597
618
  async function listSessionCatalog(options = {}) {
598
- if (closed) throw new Error('engine daemon service is closed');
619
+ if (closed) throw new Error('session service is closed');
599
620
  if (typeof listSessions !== 'function') {
600
621
  throw new Error('session catalog is unavailable');
601
622
  }
@@ -605,7 +626,7 @@ export function createEngineDaemonService({
605
626
 
606
627
  async function loadProjectStore() {
607
628
  if (typeof desktopRuntime?.loadProjects !== 'function') {
608
- throw new Error('project backend is unavailable');
629
+ throw new Error('project service is unavailable');
609
630
  }
610
631
  projectStorePromise ??= Promise.resolve(desktopRuntime.loadProjects()).catch((error) => {
611
632
  projectStorePromise = null;
@@ -678,15 +699,24 @@ export function createEngineDaemonService({
678
699
  }
679
700
 
680
701
  async function configureSession(params = {}, ctx = null) {
681
- void ctx;
682
- return runSessionAction(params, SESSION_CONFIGURE_ACTION_SET);
702
+ const revision = Math.max(0, Number(ctx?.revision) || 0);
703
+ const action = revision < 1 && params?.action === 'setBackend'
704
+ ? 'setChannelProvider'
705
+ : params?.action;
706
+ // Revision 0 desktop adapters routed some reads through configure because
707
+ // their local read list lagged the session surface. A newer daemon accepts
708
+ // those reads without weakening the current revision's finite lanes.
709
+ if (revision < 1 && SESSION_READ_ACTION_SET.has(String(action || ''))) {
710
+ return runSessionAction({ ...params, action }, SESSION_READ_ACTION_SET);
711
+ }
712
+ return runSessionAction({ ...params, action }, SESSION_CONFIGURE_ACTION_SET);
683
713
  }
684
714
 
685
715
  // ── Durable session protocol ───────────────────────────────────────────────
686
716
  // A connection is only a subscription. Session execution is accepted,
687
717
  // queued, and owned here; unsubscribe/client death never calls abort or
688
718
  // dispose. The client addresses a durable session id instead of a
689
- // client-owned engine handle.
719
+ // client-owned session runtime handle.
690
720
 
691
721
  function sessionResult(
692
722
  entry,
@@ -703,7 +733,7 @@ export function createEngineDaemonService({
703
733
  }
704
734
 
705
735
  async function createSession(params = {}, ctx = null) {
706
- if (closed) throw new Error('engine daemon service is closed');
736
+ if (closed) throw new Error('session service is closed');
707
737
  const requestedId = String(params.sessionId || '').trim();
708
738
  if (requestedId) {
709
739
  if (!/^[A-Za-z0-9_-]+$/.test(requestedId)) throw new TypeError('sessionId is invalid');
@@ -714,18 +744,19 @@ export function createEngineDaemonService({
714
744
  return sessionResult(owner, step);
715
745
  }
716
746
  }
717
- const entry = await createEntry(params, ctx);
747
+ const reservedSessionId = requestedId
748
+ || `sess_daemon_${Date.now()}_${randomUUID().replaceAll('-', '')}`;
749
+ const entry = await createEntry({ ...params, sessionId: reservedSessionId }, ctx);
718
750
  try {
719
751
  let sessionId = currentSessionId(entry);
720
752
  if (!sessionId) {
721
- sessionId = requestedId
722
- || `sess_daemon_${Date.now()}_${randomUUID().replaceAll('-', '')}`;
753
+ sessionId = reservedSessionId;
723
754
  if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) throw new TypeError('sessionId is invalid');
724
- const target = entry.engine.reserveSession;
755
+ const target = entry.runtime.reserveSession;
725
756
  if (typeof target !== 'function') {
726
- throw new TypeError('engine method reserveSession is unavailable');
757
+ throw new TypeError('session action reserveSession is unavailable');
727
758
  }
728
- await target.call(entry.engine, sessionId);
759
+ await target.call(entry.runtime, sessionId);
729
760
  entry.reservedOnly = true;
730
761
  sessionId = currentSessionId(entry);
731
762
  }
@@ -780,7 +811,7 @@ export function createEngineDaemonService({
780
811
  const token = subscriberToken(ctx);
781
812
  if (token) entry.subscribers?.delete(token);
782
813
  if ((entry.subscribers?.size || 0) === 0) {
783
- if (entry.reservedOnly && !engineBusy(entry)) {
814
+ if (entry.reservedOnly && !sessionBusy(entry)) {
784
815
  await destroy(entry, 'unclaimed session reservation', {
785
816
  keepBackgroundWork: true,
786
817
  });
@@ -800,14 +831,16 @@ export function createEngineDaemonService({
800
831
  const id = String(sessionId || '');
801
832
  if (!id) throw new TypeError('sessionId is required');
802
833
  const entry = await entryForSession(id, openHints || {});
803
- const target = typeof entry.engine.submitAsync === 'function'
804
- ? entry.engine.submitAsync
805
- : entry.engine.submit;
806
- if (typeof target !== 'function') throw new TypeError('engine submit intake is unavailable');
834
+ const target = entry.runtime.submitAsync;
835
+ if (typeof target !== 'function') throw new TypeError('session runtime must implement submitAsync');
836
+ const intake = await preparePromptSubmissionForProvider(
837
+ materializePromptSubmission(prompt, options || {}),
838
+ entry.runtime.provider || entry.runtime.session?.provider || '',
839
+ );
807
840
  // Await intake only: submitAsync resolves once the prompt is represented by
808
841
  // the queue/user row, while provider execution remains daemon-owned and
809
- // detached. Legacy embedders with synchronous submit retain the fallback.
810
- const accepted = await Promise.resolve(target.call(entry.engine, prompt, options || {}));
842
+ // detached.
843
+ const accepted = await Promise.resolve(target.call(entry.runtime, intake.prompt, intake.options));
811
844
  const firstSubmit = accepted === true && entry.reservedOnly;
812
845
  if (accepted === true) {
813
846
  entry.reservedOnly = false;
@@ -831,9 +864,9 @@ export function createEngineDaemonService({
831
864
  const id = String(sessionId || '');
832
865
  if (!id) throw new TypeError('sessionId is required');
833
866
  const entry = await entryForSession(id, openHints || {});
834
- const target = entry.engine.abort;
835
- if (typeof target !== 'function') throw new TypeError('engine method abort is unavailable');
836
- const rawResult = await target.call(entry.engine, options || {});
867
+ const target = entry.runtime.abort;
868
+ if (typeof target !== 'function') throw new TypeError('session action abort is unavailable');
869
+ const rawResult = await target.call(entry.runtime, options || {});
837
870
  const abortResult = rawResult && typeof rawResult === 'object'
838
871
  ? rawResult
839
872
  : { aborted: rawResult === true };
@@ -849,11 +882,11 @@ export function createEngineDaemonService({
849
882
  const id = String(sessionId || '');
850
883
  if (!id) throw new TypeError('sessionId is required');
851
884
  const entry = await entryForSession(id, openHints || {});
852
- const target = entry.engine.resolveToolApproval;
885
+ const target = entry.runtime.resolveToolApproval;
853
886
  if (typeof target !== 'function') {
854
- throw new TypeError('engine method resolveToolApproval is unavailable');
887
+ throw new TypeError('session action resolveToolApproval is unavailable');
855
888
  }
856
- const approved = await target.call(entry.engine, approvalId, decision);
889
+ const approved = await target.call(entry.runtime, approvalId, decision);
857
890
  const step = advance(entry);
858
891
  if (step.changed) publishStep(entry, step);
859
892
  retainUnwatched(entry, 'headless session approval');
@@ -872,12 +905,13 @@ export function createEngineDaemonService({
872
905
  if (entry.timer) { clearTimeout(entry.timer); entry.timer = null; }
873
906
  try { entry.unsubscribe?.(); } catch {}
874
907
  sessions.delete(entry);
908
+ stopEvictionSweepIfIdle();
875
909
  if (entry.indexedSessionId && sessionsById.get(entry.indexedSessionId) === entry) {
876
910
  sessionsById.delete(entry.indexedSessionId);
877
911
  }
878
912
  if (entry.busy === true) busyEntries = Math.max(0, busyEntries - 1);
879
913
  entry.busy = false;
880
- try { await entry.engine.dispose?.(reason, { keepBackgroundWork }); }
914
+ try { await entry.runtime.dispose?.(reason, { keepBackgroundWork }); }
881
915
  catch (err) { log(`session dispose failed session=${sessionId}: ${err?.message || err}`); }
882
916
  if (announce && sessionId) {
883
917
  onFrame({
@@ -891,7 +925,7 @@ export function createEngineDaemonService({
891
925
  return { ok: true };
892
926
  }
893
927
 
894
- // ── Desktop backend adapter ────────────────────────────────────────────────
928
+ // ── Desktop service adapter ────────────────────────────────────────────────
895
929
  // The adapter is a BUILD artifact supplied by the desktop install, but it is
896
930
  // instantiated here. Electron and its renderer only keep a transport view;
897
931
  // project/session/capability execution therefore shares this daemon process
@@ -901,11 +935,11 @@ export function createEngineDaemonService({
901
935
  if (kind === 'session-state') {
902
936
  return `desktop-event:${desktopId}:${kind}:${String(message?.sessionId || '')}`;
903
937
  }
904
- // Backend events are NOT one lane. Under one shared `backend-event` key a
938
+ // Service events are NOT one lane. Under one shared `desktop-event` key a
905
939
  // terminal flood clobbered LSP/folder events — and every other terminal —
906
940
  // whenever the stream backed up, because the backlog is latest-wins per
907
941
  // key. Name (and terminal id) keep each producer on its own key.
908
- if (kind === 'backend-event') {
942
+ if (kind === 'desktop-event') {
909
943
  const name = String(message?.name || '');
910
944
  const terminalId = name === 'terminal-data' ? String(message?.value?.id || '') : '';
911
945
  return `desktop-event:${desktopId}:${kind}:${name}${terminalId ? `:${terminalId}` : ''}`;
@@ -915,18 +949,18 @@ export function createEngineDaemonService({
915
949
 
916
950
  function publishDesktopEvent(desktopId, message) {
917
951
  const wire = sanitizeForWire(message);
918
- const backend = desktopBackendsById.get(desktopId);
919
- if (!wire || !backend) return;
952
+ const service = desktopServicesById.get(desktopId);
953
+ if (!wire || !service) return;
920
954
  onFrame({
921
955
  type: 'desktop-event',
922
956
  key: desktopEventKey(desktopId, wire),
923
957
  desktopId,
924
958
  message: wire,
925
- }, backend.subscribers);
959
+ }, service.subscribers);
926
960
  }
927
961
 
928
- async function initializeDesktopBackend({ desktopId, moduleUrl, options = {} } = {}) {
929
- if (closed) throw new Error('engine daemon service is closed');
962
+ async function initializeDesktopService({ desktopId, moduleUrl, options = {} } = {}) {
963
+ if (closed) throw new Error('session service is closed');
930
964
  const requestedId = String(desktopId || '').trim();
931
965
  if (!requestedId || !/^[A-Za-z0-9_-]+$/.test(requestedId)) {
932
966
  throw new TypeError('desktopId is invalid');
@@ -934,27 +968,27 @@ export function createEngineDaemonService({
934
968
  const requestedModule = String(moduleUrl || '').trim();
935
969
  let parsed;
936
970
  try { parsed = new URL(requestedModule); }
937
- catch { throw new TypeError('desktop backend moduleUrl is invalid'); }
971
+ catch { throw new TypeError('desktop service moduleUrl is invalid'); }
938
972
  if (parsed.protocol !== 'file:') {
939
- throw new TypeError('desktop backend moduleUrl must be a file URL');
973
+ throw new TypeError('desktop service moduleUrl must be a file URL');
940
974
  }
941
- const existingByModule = desktopBackendsByModule.get(requestedModule);
975
+ const existingByModule = desktopServicesByModule.get(requestedModule);
942
976
  if (existingByModule) return existingByModule;
943
- const existingById = desktopBackendsById.get(requestedId);
977
+ const existingById = desktopServicesById.get(requestedId);
944
978
  if (existingById) {
945
979
  if (existingById.moduleUrl !== requestedModule) {
946
- throw new Error(`desktopId ${requestedId} is already bound to another backend module`);
980
+ throw new Error(`desktopId ${requestedId} is already bound to another service module`);
947
981
  }
948
982
  return existingById;
949
983
  }
950
- const pending = desktopBackendPromises.get(requestedModule);
984
+ const pending = desktopServicePromises.get(requestedModule);
951
985
  if (pending) return pending;
952
986
  const loading = (async () => {
953
- const loaded = await loadDesktopBackendModule(requestedModule);
954
- if (typeof loaded.createDesktopBackend !== 'function') {
955
- throw new TypeError('desktop backend module has no createDesktopBackend export');
987
+ const loaded = await loadDesktopServiceModule(requestedModule);
988
+ if (typeof loaded.createDesktopService !== 'function') {
989
+ throw new TypeError('desktop service module has no createDesktopService export');
956
990
  }
957
- const instance = await loaded.createDesktopBackend({
991
+ const instance = await loaded.createDesktopService({
958
992
  options: sanitizeForWire(options) || {},
959
993
  runtime: desktopRuntime,
960
994
  emit: (message) => publishDesktopEvent(requestedId, message),
@@ -964,7 +998,7 @@ export function createEngineDaemonService({
964
998
  });
965
999
  if (!instance || typeof instance.invoke !== 'function'
966
1000
  || typeof instance.control !== 'function') {
967
- throw new TypeError('desktop backend adapter is invalid');
1001
+ throw new TypeError('desktop service adapter is invalid');
968
1002
  }
969
1003
  const record = {
970
1004
  desktopId: requestedId,
@@ -972,56 +1006,56 @@ export function createEngineDaemonService({
972
1006
  instance,
973
1007
  subscribers: new Set(),
974
1008
  };
975
- desktopBackendsById.set(requestedId, record);
976
- desktopBackendsByModule.set(requestedModule, record);
977
- log(`desktop backend loaded id=${requestedId} module=${requestedModule}`);
1009
+ desktopServicesById.set(requestedId, record);
1010
+ desktopServicesByModule.set(requestedModule, record);
1011
+ log(`desktop service loaded id=${requestedId} module=${requestedModule}`);
978
1012
  return record;
979
1013
  })();
980
- desktopBackendPromises.set(requestedModule, loading);
1014
+ desktopServicePromises.set(requestedModule, loading);
981
1015
  try {
982
1016
  return await loading;
983
1017
  } finally {
984
- if (desktopBackendPromises.get(requestedModule) === loading) {
985
- desktopBackendPromises.delete(requestedModule);
1018
+ if (desktopServicePromises.get(requestedModule) === loading) {
1019
+ desktopServicePromises.delete(requestedModule);
986
1020
  }
987
1021
  }
988
1022
  }
989
1023
 
990
1024
  async function desktopInit(params = {}, ctx = null) {
991
- const backend = await initializeDesktopBackend(params);
1025
+ const service = await initializeDesktopService(params);
992
1026
  const token = subscriberToken(ctx);
993
- if (token) backend.subscribers.add(token);
994
- return { desktopId: backend.desktopId };
1027
+ if (token) service.subscribers.add(token);
1028
+ return { desktopId: service.desktopId };
995
1029
  }
996
1030
 
997
- function requireDesktopBackend(desktopId) {
1031
+ function requireDesktopService(desktopId) {
998
1032
  const id = String(desktopId || '');
999
- const backend = desktopBackendsById.get(id);
1000
- if (!backend) throw new Error('desktop backend is not initialized');
1001
- return backend;
1033
+ const service = desktopServicesById.get(id);
1034
+ if (!service) throw new Error('desktop service is not initialized');
1035
+ return service;
1002
1036
  }
1003
1037
 
1004
1038
  async function desktopInvoke({ desktopId, method, args = [] } = {}, ctx = null) {
1005
- const backend = requireDesktopBackend(desktopId);
1039
+ const service = requireDesktopService(desktopId);
1006
1040
  const token = subscriberToken(ctx);
1007
- if (token) backend.subscribers.add(token);
1041
+ if (token) service.subscribers.add(token);
1008
1042
  const name = String(method || '');
1009
- if (!name) throw new TypeError('desktop backend method is required');
1010
- return sanitizeForWire(await backend.instance.invoke(name, Array.isArray(args) ? args : [])) ?? null;
1043
+ if (!name) throw new TypeError('desktop service method is required');
1044
+ return sanitizeForWire(await service.instance.invoke(name, Array.isArray(args) ? args : [])) ?? null;
1011
1045
  }
1012
1046
 
1013
1047
  async function desktopControl({ desktopId, message } = {}, ctx = null) {
1014
- const backend = requireDesktopBackend(desktopId);
1048
+ const service = requireDesktopService(desktopId);
1015
1049
  const token = subscriberToken(ctx);
1016
- if (token) backend.subscribers.add(token);
1017
- await backend.instance.control(sanitizeForWire(message) || {});
1050
+ if (token) service.subscribers.add(token);
1051
+ await service.instance.control(sanitizeForWire(message) || {});
1018
1052
  return { ok: true };
1019
1053
  }
1020
1054
 
1021
1055
  async function desktopUnsubscribe({ desktopId } = {}, ctx = null) {
1022
- const backend = requireDesktopBackend(desktopId);
1056
+ const service = requireDesktopService(desktopId);
1023
1057
  const token = subscriberToken(ctx);
1024
- if (token) backend.subscribers.delete(token);
1058
+ if (token) service.subscribers.delete(token);
1025
1059
  return { ok: true, unsubscribed: true };
1026
1060
  }
1027
1061
 
@@ -1050,21 +1084,21 @@ export function createEngineDaemonService({
1050
1084
 
1051
1085
  async function handleCall(name, args = {}, ctx = null) {
1052
1086
  const route = routes[String(name || '')];
1053
- if (!route) throw new Error(`unknown engine daemon call ${name}`);
1087
+ if (!route) throw new Error(`unknown session service call ${name}`);
1054
1088
  return route(args || {}, ctx);
1055
1089
  }
1056
1090
 
1057
1091
  async function stop(reason = 'service stop') {
1058
1092
  closed = true;
1059
1093
  if (evictTimer) { clearInterval(evictTimer); evictTimer = null; }
1060
- const backends = [...new Set(desktopBackendsById.values())];
1061
- desktopBackendsById.clear();
1062
- desktopBackendsByModule.clear();
1063
- desktopBackendPromises.clear();
1064
- for (const backend of backends) {
1065
- if (!backend?.instance?.dispose) continue;
1066
- try { await backend.instance.dispose(reason); }
1067
- catch (error) { log(`desktop backend dispose failed: ${error?.message || error}`); }
1094
+ const services = [...new Set(desktopServicesById.values())];
1095
+ desktopServicesById.clear();
1096
+ desktopServicesByModule.clear();
1097
+ desktopServicePromises.clear();
1098
+ for (const service of services) {
1099
+ if (!service?.instance?.dispose) continue;
1100
+ try { await service.instance.dispose(reason); }
1101
+ catch (error) { log(`desktop service dispose failed: ${error?.message || error}`); }
1068
1102
  }
1069
1103
  for (const entry of [...sessions]) {
1070
1104
  await destroy(entry, reason);
@@ -1093,13 +1127,14 @@ export function createEngineDaemonService({
1093
1127
  watched,
1094
1128
  retained,
1095
1129
  projected,
1130
+ evictionSweepActive: evictTimer !== null,
1096
1131
  };
1097
1132
  },
1098
1133
  /** Phone clients hosted by daemon-owned desktop adapters. */
1099
1134
  get externalClientCount() {
1100
1135
  let total = 0;
1101
- for (const backend of desktopBackendsById.values()) {
1102
- const count = Number(backend?.instance?.clientCount ?? 0);
1136
+ for (const service of desktopServicesById.values()) {
1137
+ const count = Number(service?.instance?.clientCount ?? 0);
1103
1138
  if (Number.isSafeInteger(count) && count > 0) total += count;
1104
1139
  }
1105
1140
  return total;