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.
- package/README.md +0 -17
- package/package.json +16 -8
- package/scripts/build-tui.mjs +6 -6
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/src/defaults/skills/setup/SKILL.md +1 -1
- package/src/lib/keychain-cjs.cjs +1 -1
- package/src/lib/mixdog-debug.cjs +2 -3
- package/src/rules/agent/30-explorer.md +43 -32
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +11 -10
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/agent-trace.mjs +19 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +5 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +23 -1
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +42 -26
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +44 -6
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +3 -6
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +103 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +10 -30
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -105
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
- package/src/runtime/attachments/pdf-extract.mjs +83 -0
- package/src/runtime/attachments/store.mjs +515 -0
- package/src/runtime/attachments/store.test.mjs +188 -0
- package/src/runtime/channels/lib/config.mjs +33 -33
- package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
- package/src/runtime/channels/lib/interaction-handlers.mjs +4 -194
- package/src/runtime/channels/lib/memory-client.mjs +31 -157
- package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
- package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
- package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
- package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
- package/src/runtime/channels/lib/runtime-paths.mjs +5 -34
- package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
- package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
- package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +4 -70
- package/src/runtime/channels/lib/worker-main.mjs +39 -58
- package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
- package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +72 -211
- package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
- package/src/runtime/memory/lib/http-router.mjs +1 -6
- package/src/runtime/memory/lib/http-wire.mjs +39 -4
- package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
- package/src/runtime/memory/lib/memory-log.mjs +1 -1
- package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- package/src/runtime/shared/child-spawn-gate.mjs +101 -63
- package/src/runtime/shared/config.mjs +11 -24
- package/src/runtime/shared/json-metrics.mjs +94 -0
- package/src/runtime/shared/llm/index.mjs +1 -1
- package/src/runtime/shared/owner-fair-gate.mjs +134 -0
- package/src/runtime/shared/resource-admission.mjs +6 -5
- package/src/runtime/shared/tool-execution-owner.mjs +12 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +91 -22
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +30 -2
- package/src/runtime/shared/turn-worktree-snapshot.mjs +19 -0
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-lifecycle.mjs +27 -27
- package/src/session-runtime/lifecycle-api.mjs +22 -3
- package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-models.mjs +12 -2
- package/src/session-runtime/quick-model-rows.mjs +38 -7
- package/src/session-runtime/quick-search-models.mjs +15 -17
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +61 -32
- package/src/session-runtime/runtime-tunables.mjs +9 -6
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +36 -15
- package/src/session-runtime/session-title.mjs +28 -4
- package/src/session-runtime/session-turn-api.mjs +55 -15
- package/src/session-runtime/settings-api.mjs +7 -7
- package/src/standalone/agent-dispatch-broker.mjs +1 -1
- package/src/standalone/channel-admin.mjs +42 -53
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +73 -58
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +190 -132
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
- package/src/standalone/session-protocol.mjs +9 -4
- package/src/standalone/session-runtime-pool.mjs +393 -0
- package/src/standalone/session-runtime-record.mjs +20 -0
- package/src/standalone/session-runtime-worker.mjs +203 -0
- package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +192 -157
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +142 -63
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +4 -4
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/onboarding-steps.mjs +2 -2
- package/src/tui/app/project-picker.mjs +8 -8
- package/src/tui/app/prompt-submit.mjs +15 -17
- package/src/tui/app/settings-picker.mjs +25 -25
- package/src/tui/app/use-prompt-handlers.mjs +7 -3
- package/src/tui/app/use-prompt-queue-history.mjs +1 -1
- package/src/tui/app/use-transcript-scroll.mjs +1 -1
- package/src/tui/components/ToolExecution.jsx +1 -1
- package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +403 -170
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +11 -11
- package/src/tui/paste-attachments.mjs +13 -7
- package/src/tui/prompt-history-store.mjs +52 -4
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +9 -5
- package/src/tui/{engine → session}/boot-profile.mjs +2 -2
- package/src/tui/{engine → session}/context-state.mjs +3 -3
- package/src/tui/{engine → session}/labels.mjs +2 -2
- package/src/tui/{engine → session}/live-share.mjs +28 -14
- package/src/tui/{engine → session}/notice-text.mjs +2 -2
- package/src/tui/{engine → session}/notification-plan.mjs +3 -3
- package/src/tui/session/oauth-flows.mjs +140 -0
- package/src/tui/{engine → session}/prompt-history.mjs +3 -3
- package/src/tui/{engine → session}/queue-helpers.mjs +8 -8
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +42 -21
- package/src/tui/{engine → session}/session-api.mjs +17 -61
- package/src/tui/{engine → session}/session-flow.mjs +5 -3
- package/src/tui/{engine → session}/session-stats.mjs +3 -3
- package/src/tui/{engine → session}/tool-approval.mjs +3 -3
- package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
- package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +29 -107
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +72 -78
- package/src/tui/session.mjs +22 -0
- package/src/ui/statusline-segments.mjs +1 -1
- package/src/ui/statusline.mjs +1 -1
- package/vendor/ink/build/components/App.js +10 -1
- package/src/session-runtime/memory-daemon-probe.mjs +0 -61
- package/src/standalone/engine-daemon-protocol.mjs +0 -32
- package/src/tui/engine/turn-watchdog.mjs +0 -92
- package/src/tui/engine.mjs +0 -22
- package/src/tui/hooks/useEngine.mjs +0 -16
- /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
- /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
- /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
- /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
- /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Session runtime pool hosted by the machine-global daemon.
|
|
2
2
|
//
|
|
3
|
-
// One process owns every live session
|
|
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
|
|
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
|
|
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
|
|
24
|
+
const requireDesktopService = createRequire(import.meta.url);
|
|
22
25
|
|
|
23
|
-
async function
|
|
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 =
|
|
34
|
-
delete
|
|
35
|
-
return
|
|
36
|
+
const resolved = requireDesktopService.resolve(modulePath);
|
|
37
|
+
delete requireDesktopService.cache[resolved];
|
|
38
|
+
return requireDesktopService(resolved);
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
/** JSON-safe projection of
|
|
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
|
|
92
|
-
|
|
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
|
-
|
|
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
|
|
107
|
-
//
|
|
108
|
-
|
|
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
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
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.
|
|
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
|
|
144
|
-
//
|
|
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
|
|
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
|
|
164
|
-
* walked away, so an
|
|
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 && !
|
|
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
|
|
182
|
-
|
|
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
|
|
200
|
+
function sessionBusy(entry) {
|
|
202
201
|
if (typeof entry?.busy === 'boolean') return entry.busy;
|
|
203
202
|
try {
|
|
204
|
-
return updateEntryBusy(entry, entry.
|
|
203
|
+
return updateEntryBusy(entry, entry.runtime.getState?.() || {});
|
|
205
204
|
} catch {
|
|
206
|
-
// An
|
|
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
|
|
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 (
|
|
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.
|
|
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
|
|
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?.
|
|
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
|
|
364
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
383
|
-
// address must be FULL; only later frames may use
|
|
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.
|
|
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
|
-
/**
|
|
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('
|
|
454
|
-
const
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
//
|
|
488
|
-
// it first. The daemon resolves the session to its
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
529
|
-
* load per session at a time — concurrent panes converge on one
|
|
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
|
|
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('
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
588
|
-
// view
|
|
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.
|
|
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('
|
|
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
|
|
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
|
-
|
|
682
|
-
|
|
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
|
|
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('
|
|
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
|
|
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 =
|
|
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.
|
|
755
|
+
const target = entry.runtime.reserveSession;
|
|
725
756
|
if (typeof target !== 'function') {
|
|
726
|
-
throw new TypeError('
|
|
757
|
+
throw new TypeError('session action reserveSession is unavailable');
|
|
727
758
|
}
|
|
728
|
-
await target.call(entry.
|
|
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 && !
|
|
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 =
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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.
|
|
810
|
-
const accepted = await Promise.resolve(target.call(entry.
|
|
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.
|
|
835
|
-
if (typeof target !== 'function') throw new TypeError('
|
|
836
|
-
const rawResult = await target.call(entry.
|
|
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.
|
|
885
|
+
const target = entry.runtime.resolveToolApproval;
|
|
853
886
|
if (typeof target !== 'function') {
|
|
854
|
-
throw new TypeError('
|
|
887
|
+
throw new TypeError('session action resolveToolApproval is unavailable');
|
|
855
888
|
}
|
|
856
|
-
const approved = await target.call(entry.
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
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 === '
|
|
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
|
|
919
|
-
if (!wire || !
|
|
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
|
-
},
|
|
959
|
+
}, service.subscribers);
|
|
926
960
|
}
|
|
927
961
|
|
|
928
|
-
async function
|
|
929
|
-
if (closed) throw new Error('
|
|
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
|
|
971
|
+
catch { throw new TypeError('desktop service moduleUrl is invalid'); }
|
|
938
972
|
if (parsed.protocol !== 'file:') {
|
|
939
|
-
throw new TypeError('desktop
|
|
973
|
+
throw new TypeError('desktop service moduleUrl must be a file URL');
|
|
940
974
|
}
|
|
941
|
-
const existingByModule =
|
|
975
|
+
const existingByModule = desktopServicesByModule.get(requestedModule);
|
|
942
976
|
if (existingByModule) return existingByModule;
|
|
943
|
-
const existingById =
|
|
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
|
|
980
|
+
throw new Error(`desktopId ${requestedId} is already bound to another service module`);
|
|
947
981
|
}
|
|
948
982
|
return existingById;
|
|
949
983
|
}
|
|
950
|
-
const pending =
|
|
984
|
+
const pending = desktopServicePromises.get(requestedModule);
|
|
951
985
|
if (pending) return pending;
|
|
952
986
|
const loading = (async () => {
|
|
953
|
-
const loaded = await
|
|
954
|
-
if (typeof loaded.
|
|
955
|
-
throw new TypeError('desktop
|
|
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.
|
|
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
|
|
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
|
-
|
|
976
|
-
|
|
977
|
-
log(`desktop
|
|
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
|
-
|
|
1014
|
+
desktopServicePromises.set(requestedModule, loading);
|
|
981
1015
|
try {
|
|
982
1016
|
return await loading;
|
|
983
1017
|
} finally {
|
|
984
|
-
if (
|
|
985
|
-
|
|
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
|
|
1025
|
+
const service = await initializeDesktopService(params);
|
|
992
1026
|
const token = subscriberToken(ctx);
|
|
993
|
-
if (token)
|
|
994
|
-
return { desktopId:
|
|
1027
|
+
if (token) service.subscribers.add(token);
|
|
1028
|
+
return { desktopId: service.desktopId };
|
|
995
1029
|
}
|
|
996
1030
|
|
|
997
|
-
function
|
|
1031
|
+
function requireDesktopService(desktopId) {
|
|
998
1032
|
const id = String(desktopId || '');
|
|
999
|
-
const
|
|
1000
|
-
if (!
|
|
1001
|
-
return
|
|
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
|
|
1039
|
+
const service = requireDesktopService(desktopId);
|
|
1006
1040
|
const token = subscriberToken(ctx);
|
|
1007
|
-
if (token)
|
|
1041
|
+
if (token) service.subscribers.add(token);
|
|
1008
1042
|
const name = String(method || '');
|
|
1009
|
-
if (!name) throw new TypeError('desktop
|
|
1010
|
-
return sanitizeForWire(await
|
|
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
|
|
1048
|
+
const service = requireDesktopService(desktopId);
|
|
1015
1049
|
const token = subscriberToken(ctx);
|
|
1016
|
-
if (token)
|
|
1017
|
-
await
|
|
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
|
|
1056
|
+
const service = requireDesktopService(desktopId);
|
|
1023
1057
|
const token = subscriberToken(ctx);
|
|
1024
|
-
if (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
|
|
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
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
for (const
|
|
1065
|
-
if (!
|
|
1066
|
-
try { await
|
|
1067
|
-
catch (error) { log(`desktop
|
|
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
|
|
1102
|
-
const count = Number(
|
|
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;
|