mixdog 0.9.151 → 0.9.152

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 (175) hide show
  1. package/README.md +110 -68
  2. package/package.json +3 -2
  3. package/scripts/tool-stress.mjs +0 -2
  4. package/src/defaults/skills/setup/SKILL.md +18 -27
  5. package/src/headless-exec.mjs +3 -10
  6. package/src/headless-exec.test.mjs +2 -7
  7. package/src/runtime/agent/orchestrator/config.mjs +6 -66
  8. package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
  9. package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
  10. package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
  11. package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
  12. package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
  13. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
  14. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
  15. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  16. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  17. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
  18. package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
  19. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
  20. package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
  21. package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
  22. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
  23. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
  24. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
  25. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
  26. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
  27. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
  28. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
  29. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
  30. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
  31. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
  32. package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
  33. package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
  34. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
  35. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
  36. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
  37. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
  38. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
  39. package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
  40. package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
  41. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
  43. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
  44. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
  45. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
  46. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
  47. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
  48. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
  49. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
  50. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
  51. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
  52. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
  53. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
  54. package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
  55. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
  56. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
  57. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
  58. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
  59. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
  60. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
  61. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
  62. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
  63. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
  64. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
  65. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
  66. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
  67. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
  68. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
  69. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
  70. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
  71. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
  72. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
  73. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
  74. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  75. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  76. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
  77. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  78. package/src/runtime/browser-bridge/client.mjs +103 -0
  79. package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
  80. package/src/runtime/computer-bridge/client.mjs +101 -0
  81. package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
  82. package/src/runtime/media/store.mjs +18 -8
  83. package/src/runtime/media/store.test.mjs +14 -0
  84. package/src/runtime/memory/index.mjs +1 -1
  85. package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
  86. package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
  87. package/src/runtime/shared/child-spawn-gate.mjs +17 -0
  88. package/src/runtime/shared/child-spawn-remote.mjs +69 -7
  89. package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
  90. package/src/runtime/shared/pristine-execution-contract.json +2 -1
  91. package/src/runtime/shared/provider-api-key.mjs +1 -0
  92. package/src/runtime/shared/session-runtime-health.mjs +91 -0
  93. package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
  94. package/src/runtime/shared/skill-document.mjs +86 -0
  95. package/src/runtime/shared/skill-document.test.mjs +81 -0
  96. package/src/runtime/shared/tool-surface.mjs +18 -0
  97. package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
  98. package/src/runtime/shared/turn-snapshot.mjs +32 -9
  99. package/src/runtime/shared/user-cwd.mjs +33 -14
  100. package/src/runtime/shared/user-cwd.test.mjs +43 -0
  101. package/src/session-runtime/cwd-plugins.mjs +26 -26
  102. package/src/session-runtime/cwd-plugins.test.mjs +75 -0
  103. package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
  104. package/src/session-runtime/global-extensions.mjs +26 -0
  105. package/src/session-runtime/global-extensions.test.mjs +28 -0
  106. package/src/session-runtime/goal-runtime.mjs +721 -0
  107. package/src/session-runtime/goal-runtime.test.mjs +108 -0
  108. package/src/session-runtime/lifecycle-api.mjs +18 -4
  109. package/src/session-runtime/lifecycle-api.test.mjs +21 -0
  110. package/src/session-runtime/mcp-glue.mjs +49 -47
  111. package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
  112. package/src/session-runtime/plugin-mcp.mjs +96 -22
  113. package/src/session-runtime/prewarm.mjs +1 -10
  114. package/src/session-runtime/resource-api-global.test.mjs +69 -0
  115. package/src/session-runtime/resource-api.mjs +173 -58
  116. package/src/session-runtime/runtime-core.mjs +148 -20
  117. package/src/session-runtime/runtime-tunables.mjs +6 -8
  118. package/src/session-runtime/session-turn-api.mjs +22 -11
  119. package/src/session-runtime/skills-api.mjs +79 -25
  120. package/src/session-runtime/skills-api.test.mjs +99 -0
  121. package/src/session-runtime/tool-catalog-schema.mjs +0 -5
  122. package/src/session-runtime/tool-catalog.mjs +4 -13
  123. package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
  124. package/src/session-runtime/tool-surface.mjs +12 -0
  125. package/src/standalone/agent-dispatch-broker.mjs +5 -9
  126. package/src/standalone/agent-tool/notify.mjs +6 -8
  127. package/src/standalone/agent-tool/notify.test.mjs +37 -0
  128. package/src/standalone/daemon.mjs +31 -15
  129. package/src/standalone/hook-bus/config.mjs +1 -0
  130. package/src/standalone/plugin-admin.mjs +17 -0
  131. package/src/standalone/plugin-admin.test.mjs +7 -0
  132. package/src/standalone/provider-admin.mjs +1 -0
  133. package/src/standalone/session-protocol.mjs +5 -0
  134. package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
  135. package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
  136. package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
  137. package/src/standalone/session-runtime-host-factory.mjs +17 -0
  138. package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
  139. package/src/standalone/session-runtime-host-health.test.mjs +5 -0
  140. package/src/standalone/session-runtime-host.mjs +696 -58
  141. package/src/standalone/session-runtime-inline-host.mjs +351 -0
  142. package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
  143. package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
  144. package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
  145. package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
  146. package/src/standalone/session-runtime-shard-router.mjs +149 -0
  147. package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
  148. package/src/standalone/session-runtime-worker.mjs +476 -18
  149. package/src/standalone/session-service.mjs +81 -6
  150. package/src/tui/app/app-view.jsx +1 -0
  151. package/src/tui/app/core-memory-picker.mjs +2 -0
  152. package/src/tui/app/extension-pickers.mjs +1 -1
  153. package/src/tui/app/maintenance-pickers.mjs +3 -3
  154. package/src/tui/app/model-options.mjs +2 -0
  155. package/src/tui/app/model-picker.mjs +14 -4
  156. package/src/tui/app/panel-handoff.test.mjs +311 -0
  157. package/src/tui/app/project-picker.mjs +1 -0
  158. package/src/tui/app/route-pickers.mjs +21 -6
  159. package/src/tui/app/settings-picker.mjs +26 -3
  160. package/src/tui/app/slash-commands.mjs +1 -0
  161. package/src/tui/app/slash-dispatch.mjs +73 -22
  162. package/src/tui/app/theme-effort-pickers.mjs +7 -2
  163. package/src/tui/components/Picker.jsx +10 -1
  164. package/src/tui/dist/index.mjs +163 -44
  165. package/src/tui/session/context-state.mjs +1 -0
  166. package/src/tui/session/goal-continuation.mjs +110 -0
  167. package/src/tui/session/goal-continuation.test.mjs +84 -0
  168. package/src/tui/session/queue-helpers.mjs +5 -2
  169. package/src/tui/session/session-action-surface.test.mjs +108 -0
  170. package/src/tui/session/session-api-ext.mjs +39 -8
  171. package/src/tui/session/session-api.mjs +58 -0
  172. package/src/tui/session/session-flow.mjs +6 -0
  173. package/src/tui/session/turn.mjs +1 -0
  174. package/src/tui/session-local.mjs +15 -1
  175. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
@@ -0,0 +1,351 @@
1
+ /**
2
+ * In-process session runtime host.
3
+ *
4
+ * Sessions remain independent async actors, but share the daemon's V8 isolate
5
+ * and ESM module cache. Native helpers and explicitly bounded worker pools keep
6
+ * CPU-heavy parsing/search work off the daemon loop.
7
+ */
8
+ export function createInlineSessionRuntimeHost({
9
+ cwd = process.cwd(),
10
+ log = () => {},
11
+ loadLocalModule = () => import('../tui/session-local.mjs'),
12
+ } = {}) {
13
+ const records = new Map();
14
+ const recordsBySessionId = new Map();
15
+ const agentSessionListeners = new Set();
16
+ const agentSessionSnapshots = new Map();
17
+ const activeAgentSessionProjections = new Set();
18
+ const pendingAgentSessionProjections = new Map();
19
+ const agentDispatchers = new Map();
20
+ const agentDispatchRuns = new Map();
21
+ let nextRuntimeId = 0;
22
+ let closed = false;
23
+ let localModulePromise = null;
24
+ let prewarmPromise = null;
25
+ let agentGraphPromise = null;
26
+ let preparedProviderSignature = null;
27
+ let providerPreparePromise = null;
28
+ let unsubscribeAgentSessionPublisher = null;
29
+ let agentSessionPublisherPromise = null;
30
+
31
+ function localModule() {
32
+ localModulePromise ??= Promise.resolve().then(loadLocalModule).catch((error) => {
33
+ localModulePromise = null;
34
+ throw error;
35
+ });
36
+ return localModulePromise;
37
+ }
38
+
39
+ function agentGraph() {
40
+ agentGraphPromise ??= Promise.all([
41
+ import('../runtime/agent/orchestrator/config.mjs'),
42
+ import('../runtime/agent/orchestrator/providers/registry.mjs'),
43
+ import('../runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs'),
44
+ ]).then(
45
+ ([config, registry, dispatch]) => ({ config, registry, dispatch }),
46
+ (error) => {
47
+ agentGraphPromise = null;
48
+ throw error;
49
+ },
50
+ );
51
+ return agentGraphPromise;
52
+ }
53
+
54
+ async function prepareAgentProviders() {
55
+ const { config, registry } = await agentGraph();
56
+ const providers = config.loadConfig()?.providers || {};
57
+ let signature = null;
58
+ try { signature = JSON.stringify(providers); } catch { /* re-prepare each call */ }
59
+ if (signature !== null && preparedProviderSignature === signature) return;
60
+ if (providerPreparePromise) {
61
+ await providerPreparePromise;
62
+ if (signature !== null && preparedProviderSignature === signature) return;
63
+ return prepareAgentProviders();
64
+ }
65
+ const pending = Promise.resolve()
66
+ .then(() => registry.initProviders(providers))
67
+ .then(() => { preparedProviderSignature = signature; });
68
+ const tracked = pending.finally(() => {
69
+ if (providerPreparePromise === tracked) providerPreparePromise = null;
70
+ });
71
+ providerPreparePromise = tracked;
72
+ await tracked;
73
+ }
74
+
75
+ function agentOwnedSession(session) {
76
+ const owner = String(session?.owner || '').trim().toLowerCase();
77
+ const agent = String(session?.agent || '').trim().toLowerCase();
78
+ return Boolean(session?.id)
79
+ && (owner === 'agent' || (agent && agent !== 'lead'));
80
+ }
81
+
82
+ async function projectAgentSession(session) {
83
+ const [
84
+ { restoreTranscriptItems },
85
+ { getSessionProgressSnapshot },
86
+ ] = await Promise.all([
87
+ import('../tui/session/session-api-ext.mjs'),
88
+ import('../runtime/agent/orchestrator/session/manager.mjs'),
89
+ ]);
90
+ const sessionId = String(session.id || '');
91
+ const status = String(session.status || '').trim().toLowerCase();
92
+ const progress = getSessionProgressSnapshot?.(sessionId) || null;
93
+ const stage = String(progress?.stage || '').trim().toLowerCase();
94
+ const busy = status === 'running' || status === 'queued'
95
+ || (stage && stage !== 'idle' && stage !== 'done' && stage !== 'closed');
96
+ return {
97
+ sessionId,
98
+ items: restoreTranscriptItems(
99
+ Array.isArray(session.messages) ? session.messages : [],
100
+ { sessionId, itemLimit: 512 },
101
+ ),
102
+ queued: [],
103
+ busy,
104
+ commandBusy: false,
105
+ provider: session.provider || '',
106
+ model: session.model || '',
107
+ effort: session.effort || '',
108
+ fast: session.fast === true,
109
+ modelParameters: session.modelParameters || {},
110
+ cwd: session.cwd || '',
111
+ desktopSession: session.desktopSession || null,
112
+ workflow: session.workflow || null,
113
+ ownerClientHostPid: Number(session.clientHostPid) || process.pid,
114
+ };
115
+ }
116
+
117
+ function publishAgentSessionState(sessionId, snapshot) {
118
+ agentSessionSnapshots.delete(sessionId);
119
+ agentSessionSnapshots.set(sessionId, snapshot);
120
+ while (agentSessionSnapshots.size > 256) {
121
+ const oldest = agentSessionSnapshots.keys().next().value;
122
+ if (oldest === undefined) break;
123
+ agentSessionSnapshots.delete(oldest);
124
+ }
125
+ for (const listener of [...agentSessionListeners]) {
126
+ try { listener({ sessionId, snapshot }); } catch {}
127
+ }
128
+ }
129
+
130
+ function scheduleAgentSessionProjection(session) {
131
+ if (!agentOwnedSession(session) || closed) return;
132
+ const sessionId = String(session.id);
133
+ pendingAgentSessionProjections.set(sessionId, session);
134
+ if (activeAgentSessionProjections.has(sessionId)) return;
135
+ activeAgentSessionProjections.add(sessionId);
136
+ queueMicrotask(() => {
137
+ void (async () => {
138
+ while (!closed) {
139
+ const latest = pendingAgentSessionProjections.get(sessionId);
140
+ if (!latest) break;
141
+ pendingAgentSessionProjections.delete(sessionId);
142
+ const snapshot = await projectAgentSession(latest);
143
+ if (!closed) publishAgentSessionState(sessionId, snapshot);
144
+ }
145
+ })().catch((error) => {
146
+ log(`agent session projection failed session=${sessionId}: ${error?.message || error}`);
147
+ }).finally(() => {
148
+ activeAgentSessionProjections.delete(sessionId);
149
+ if (pendingAgentSessionProjections.has(sessionId) && !closed) {
150
+ scheduleAgentSessionProjection(pendingAgentSessionProjections.get(sessionId));
151
+ }
152
+ });
153
+ });
154
+ }
155
+
156
+ async function ensureAgentSessionPublisher() {
157
+ if (unsubscribeAgentSessionPublisher) return;
158
+ agentSessionPublisherPromise ??= import(
159
+ '../runtime/agent/orchestrator/session/store.mjs'
160
+ ).then((store) => {
161
+ unsubscribeAgentSessionPublisher = store.subscribeLiveSessions(
162
+ scheduleAgentSessionProjection,
163
+ );
164
+ }).finally(() => {
165
+ agentSessionPublisherPromise = null;
166
+ });
167
+ await agentSessionPublisherPromise;
168
+ }
169
+
170
+ function forget(record) {
171
+ if (!record || !records.delete(record.id)) return;
172
+ for (const [sessionId, owner] of recordsBySessionId) {
173
+ if (owner === record) recordsBySessionId.delete(sessionId);
174
+ }
175
+ }
176
+
177
+ function ownerRuntime(sessionId) {
178
+ const id = String(sessionId || '').trim();
179
+ const known = recordsBySessionId.get(id);
180
+ if (known) return known.runtime;
181
+ for (const record of records.values()) {
182
+ const current = String(record.runtime.getState?.()?.sessionId || record.runtime.id || '');
183
+ if (!current || current !== id) continue;
184
+ recordsBySessionId.set(id, record);
185
+ return record.runtime;
186
+ }
187
+ return null;
188
+ }
189
+
190
+ async function create(options = {}) {
191
+ if (closed) throw new Error('session runtime host is closed');
192
+ const module = await localModule();
193
+ const runtime = await module.createLocalSessionRuntime({
194
+ ...options,
195
+ ...(options.cwd ? {} : { cwd }),
196
+ });
197
+ const record = {
198
+ id: `inline-${process.pid}-${++nextRuntimeId}`,
199
+ runtime,
200
+ };
201
+ records.set(record.id, record);
202
+ const hintedSessionId = String(options.sessionId || '').trim();
203
+ if (hintedSessionId) recordsBySessionId.set(hintedSessionId, record);
204
+
205
+ const originalDispose = typeof runtime.dispose === 'function'
206
+ ? runtime.dispose.bind(runtime)
207
+ : null;
208
+ runtime.dispose = async (...args) => {
209
+ try {
210
+ return await originalDispose?.(...args);
211
+ } finally {
212
+ forget(record);
213
+ }
214
+ };
215
+ return runtime;
216
+ }
217
+
218
+ function prewarm() {
219
+ if (closed) return Promise.reject(new Error('session runtime host is closed'));
220
+ prewarmPromise ??= localModule().then((module) => Promise.allSettled([
221
+ module.preloadSessionRuntimeModule?.(),
222
+ module.preloadAgentLoopRuntime?.(),
223
+ module.preloadKeychainSecrets?.(),
224
+ ])).then(() => ({ ready: true }));
225
+ return prewarmPromise;
226
+ }
227
+
228
+ async function agentDispatch(payload = {}, { signal = null } = {}) {
229
+ if (closed) throw new Error('session runtime host is closed');
230
+ const dispatchId = String(payload?.dispatchId || '');
231
+ if (!dispatchId) throw new Error('agent dispatch id is required');
232
+ if (agentDispatchRuns.has(dispatchId)) {
233
+ throw new Error(`agent dispatch ${dispatchId} is already running`);
234
+ }
235
+ const controller = new AbortController();
236
+ const abort = () => {
237
+ if (controller.signal.aborted) return;
238
+ try { controller.abort(signal?.reason); } catch { try { controller.abort(); } catch {} }
239
+ };
240
+ if (signal?.aborted) abort();
241
+ else signal?.addEventListener?.('abort', abort, { once: true });
242
+ agentDispatchRuns.set(dispatchId, controller);
243
+ try {
244
+ if (controller.signal.aborted) throw controller.signal.reason || new Error('agent dispatch canceled');
245
+ const agent = String(payload.agent || '');
246
+ const { dispatch } = await agentGraph();
247
+ await ensureAgentSessionPublisher();
248
+ await prepareAgentProviders();
249
+ if (controller.signal.aborted) throw controller.signal.reason || new Error('agent dispatch canceled');
250
+ let dispatcher = agentDispatchers.get(agent);
251
+ if (!dispatcher) {
252
+ dispatcher = dispatch.makeAgentDispatch({
253
+ agent,
254
+ ...(payload.options && typeof payload.options === 'object' ? payload.options : {}),
255
+ });
256
+ agentDispatchers.set(agent, dispatcher);
257
+ }
258
+ const params = payload.params && typeof payload.params === 'object' ? payload.params : {};
259
+ return await dispatcher({
260
+ prompt: String(params.prompt ?? ''),
261
+ preset: params.preset || undefined,
262
+ cwd: typeof params.cwd === 'string' && params.cwd ? params.cwd : undefined,
263
+ parentSignal: controller.signal,
264
+ ...(Number.isFinite(Number(params.idleTimeoutMs)) && Number(params.idleTimeoutMs) > 0
265
+ ? { idleTimeoutMs: Number(params.idleTimeoutMs) }
266
+ : {}),
267
+ });
268
+ } finally {
269
+ signal?.removeEventListener?.('abort', abort);
270
+ agentDispatchRuns.delete(dispatchId);
271
+ }
272
+ }
273
+
274
+ return {
275
+ create,
276
+ prewarm,
277
+ agentDispatch,
278
+ async agentControl(args = {}, context = {}) {
279
+ if (closed) throw new Error('session runtime host is closed');
280
+ const runtime = ownerRuntime(context?.callerSessionId);
281
+ if (!runtime?.agentControl) {
282
+ throw new Error(`session ${context?.callerSessionId || 'unknown'} is not loaded`);
283
+ }
284
+ return runtime.agentControl(args);
285
+ },
286
+ refreshRuntimeWorkload() {
287
+ return Promise.resolve(this.workloads);
288
+ },
289
+ subscribeAgentSessionStates(listener) {
290
+ if (typeof listener !== 'function') return () => {};
291
+ agentSessionListeners.add(listener);
292
+ void ensureAgentSessionPublisher().catch((error) => {
293
+ log(`agent session publisher failed (non-fatal): ${error?.message || error}`);
294
+ });
295
+ for (const [sessionId, snapshot] of agentSessionSnapshots) {
296
+ try { listener({ sessionId, snapshot }); } catch {}
297
+ }
298
+ return () => agentSessionListeners.delete(listener);
299
+ },
300
+ get workloads() {
301
+ const memory = process.memoryUsage();
302
+ return {
303
+ mode: 'in-process',
304
+ refreshedAt: Date.now(),
305
+ shardCount: 0,
306
+ shards: [],
307
+ worker: {
308
+ pid: process.pid,
309
+ runtimes: records.size,
310
+ memory: {
311
+ rss: memory.rss,
312
+ heapTotal: memory.heapTotal,
313
+ heapUsed: memory.heapUsed,
314
+ external: memory.external,
315
+ },
316
+ },
317
+ };
318
+ },
319
+ async close(reason = 'session runtime host closed') {
320
+ if (closed) return;
321
+ closed = true;
322
+ try { unsubscribeAgentSessionPublisher?.(); } catch {}
323
+ unsubscribeAgentSessionPublisher = null;
324
+ agentSessionListeners.clear();
325
+ agentSessionSnapshots.clear();
326
+ pendingAgentSessionProjections.clear();
327
+ for (const controller of agentDispatchRuns.values()) {
328
+ try { controller.abort(new Error(reason)); } catch {}
329
+ }
330
+ agentDispatchRuns.clear();
331
+ const active = [...records.values()];
332
+ await Promise.allSettled(active.map((record) => record.runtime.dispose?.(reason)));
333
+ records.clear();
334
+ recordsBySessionId.clear();
335
+ },
336
+ get status() {
337
+ return {
338
+ mode: 'in-process',
339
+ active: !closed,
340
+ worker: {
341
+ pid: process.pid,
342
+ pids: [process.pid],
343
+ runtimes: records.size,
344
+ },
345
+ shards: [],
346
+ shardCount: 0,
347
+ providerCooldown: null,
348
+ };
349
+ },
350
+ };
351
+ }
@@ -0,0 +1,83 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { createInlineSessionRuntimeHost } from './session-runtime-inline-host.mjs';
5
+
6
+ function createFakeLocalModule(events) {
7
+ return {
8
+ async createLocalSessionRuntime(options = {}) {
9
+ events.push(['create', options]);
10
+ const listeners = new Set();
11
+ const state = { sessionId: options.sessionId || null };
12
+ return {
13
+ id: state.sessionId,
14
+ getState: () => state,
15
+ subscribe(listener) {
16
+ listeners.add(listener);
17
+ return () => listeners.delete(listener);
18
+ },
19
+ async dispose(reason) {
20
+ events.push(['dispose', reason]);
21
+ },
22
+ async agentControl(args) {
23
+ return JSON.stringify(args);
24
+ },
25
+ };
26
+ },
27
+ async preloadSessionRuntimeModule() {
28
+ events.push(['prewarm', 'runtime']);
29
+ },
30
+ async preloadAgentLoopRuntime() {
31
+ events.push(['prewarm', 'agent-loop']);
32
+ },
33
+ async preloadKeychainSecrets() {
34
+ events.push(['prewarm', 'keychain']);
35
+ },
36
+ };
37
+ }
38
+
39
+ test('inline host keeps session actors in the daemon process and releases them', async () => {
40
+ const events = [];
41
+ const host = createInlineSessionRuntimeHost({
42
+ cwd: 'C:\\project',
43
+ loadLocalModule: async () => createFakeLocalModule(events),
44
+ });
45
+
46
+ const first = await host.create({ sessionId: 'session-a' });
47
+ const second = await host.create({ sessionId: 'session-b', cwd: 'D:\\other' });
48
+
49
+ assert.equal(host.status.mode, 'in-process');
50
+ assert.equal(host.status.worker.pid, process.pid);
51
+ assert.equal(host.status.worker.runtimes, 2);
52
+ assert.equal(events[0][1].cwd, 'C:\\project');
53
+ assert.equal(events[1][1].cwd, 'D:\\other');
54
+ assert.equal(
55
+ await host.agentControl({ type: 'list' }, { callerSessionId: 'session-a' }),
56
+ '{"type":"list"}',
57
+ );
58
+
59
+ await first.dispose('idle');
60
+ assert.equal(host.status.worker.runtimes, 1);
61
+ await host.close('done');
62
+ assert.equal(host.status.active, false);
63
+ assert.equal(host.status.worker.runtimes, 0);
64
+ assert.equal(events.some(([type, reason]) => type === 'dispose' && reason === 'done'), true);
65
+ void second;
66
+ });
67
+
68
+ test('inline host prewarm deliberately leaves the memory process cold', async () => {
69
+ const events = [];
70
+ const host = createInlineSessionRuntimeHost({
71
+ loadLocalModule: async () => createFakeLocalModule(events),
72
+ });
73
+
74
+ await host.prewarm();
75
+ await host.prewarm();
76
+
77
+ assert.deepEqual(
78
+ events.filter(([type]) => type === 'prewarm').map(([, target]) => target).sort(),
79
+ ['agent-loop', 'keychain', 'runtime'],
80
+ );
81
+ await host.close('done');
82
+ });
83
+
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ // Cross-shard provider cooldown encoding.
4
+ //
5
+ // Fast-mode capacity cooldown is an ACCOUNT-wide fact kept in process-local
6
+ // policy state. With the runtime split across several shard processes, a
7
+ // cooldown discovered by one shard must reach the others or its siblings keep
8
+ // hammering a pool that is already known to be drained.
9
+ //
10
+ // The policy module owns the decision rules, so a replay is expressed through
11
+ // its PUBLIC API as the same capacity rejection the discovering shard saw:
12
+ // this file only encodes/decodes that fact. The policy applies its own minimum
13
+ // window, so a replay can only ever be equal or MORE conservative than the
14
+ // observation — never a shorter or cleared cooldown.
15
+
16
+ const ECHO_TOLERANCE_MS = 1_000;
17
+
18
+ /** Read this process's live cooldown as a wire-safe record. */
19
+ export function readProviderCooldown(policy, now = Date.now()) {
20
+ if (!policy) return null;
21
+ let remaining = 0;
22
+ let disabledReason = null;
23
+ try {
24
+ remaining = policy.fastModeCooldownRemainingMs(now);
25
+ disabledReason = policy.fastModeDisabledReason();
26
+ } catch { return null; }
27
+ return {
28
+ untilMs: Number.isFinite(remaining) && remaining > 0 ? now + remaining : 0,
29
+ disabledReason: disabledReason || null,
30
+ observedAt: now,
31
+ };
32
+ }
33
+
34
+ /** True only for a materially LONGER cooldown or a newly disabled pool. */
35
+ export function providerCooldownAdvanced(previous, next) {
36
+ const priorUntil = Number(previous?.untilMs) || 0;
37
+ const nextUntil = Number(next?.untilMs) || 0;
38
+ const nextReason = next?.disabledReason ? String(next.disabledReason) : null;
39
+ const priorReason = previous?.disabledReason ? String(previous.disabledReason) : null;
40
+ return nextUntil > priorUntil + ECHO_TOLERANCE_MS
41
+ || (Boolean(nextReason) && nextReason !== priorReason);
42
+ }
43
+
44
+ export function mergeKnownProviderCooldown(previous, next) {
45
+ return {
46
+ untilMs: Math.max(Number(previous?.untilMs) || 0, Number(next?.untilMs) || 0),
47
+ disabledReason: (next?.disabledReason ? String(next.disabledReason) : null)
48
+ || (previous?.disabledReason ? String(previous.disabledReason) : null),
49
+ };
50
+ }
51
+
52
+ /** Replay a sibling shard's cooldown into this process's policy state. */
53
+ export function applyProviderCooldown(policy, cooldown, now = Date.now()) {
54
+ if (!policy) return false;
55
+ const untilMs = Number(cooldown?.untilMs) || 0;
56
+ const disabledReason = cooldown?.disabledReason ? String(cooldown.disabledReason) : '';
57
+ const policyLimits = policy._FAST_MODE_POLICY || {};
58
+ let applied = false;
59
+ if (disabledReason && policyLimits.OVERAGE_DISABLED_HEADER) {
60
+ policy.noteFastModeCapacityError({
61
+ httpStatus: 429,
62
+ headers: { [policyLimits.OVERAGE_DISABLED_HEADER]: disabledReason },
63
+ }, { fast: true, now });
64
+ applied = true;
65
+ }
66
+ const remainingMs = untilMs - now;
67
+ // Below the policy's short-retry bound the discovering shard itself keeps
68
+ // fast mode, so there is nothing to replicate.
69
+ const shortRetryMs = Number(policyLimits.SHORT_RETRY_THRESHOLD_MS) || 0;
70
+ if (remainingMs >= shortRetryMs && remainingMs > 0) {
71
+ policy.noteFastModeCapacityError(
72
+ { httpStatus: 429, retryAfterMs: remainingMs },
73
+ { fast: true, now },
74
+ );
75
+ applied = true;
76
+ }
77
+ return applied;
78
+ }
@@ -0,0 +1,80 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import * as fastMode from '../runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs';
5
+ import {
6
+ applyProviderCooldown,
7
+ mergeKnownProviderCooldown,
8
+ providerCooldownAdvanced,
9
+ readProviderCooldown,
10
+ } from './session-runtime-provider-cooldown.mjs';
11
+
12
+ test('a sibling shard cooldown is replayed into this process without hammering the pool', () => {
13
+ fastMode.clearFastModeCooldown();
14
+ try {
15
+ const now = Date.now();
16
+ assert.equal(fastMode.fastModeAvailable(now), true);
17
+
18
+ const applied = applyProviderCooldown(fastMode, { untilMs: now + 120_000 }, now);
19
+ assert.equal(applied, true);
20
+ assert.equal(fastMode.fastModeAvailable(now), false);
21
+ // The policy owns the floor: a replay is never SHORTER than observed.
22
+ assert.ok(fastMode.fastModeCooldownRemainingMs(now) >= 120_000);
23
+
24
+ // Reading it back reproduces the cooldown for the next shard in line.
25
+ const observed = readProviderCooldown(fastMode, now);
26
+ assert.ok(observed.untilMs >= now + 120_000);
27
+ assert.equal(observed.disabledReason, null);
28
+ } finally {
29
+ fastMode.clearFastModeCooldown();
30
+ }
31
+ });
32
+
33
+ test('an expired or short replay never clears a live cooldown', () => {
34
+ fastMode.clearFastModeCooldown();
35
+ try {
36
+ const now = Date.now();
37
+ applyProviderCooldown(fastMode, { untilMs: now + 300_000 }, now);
38
+ const remaining = fastMode.fastModeCooldownRemainingMs(now);
39
+ // Stale/short frames are ignored; the live cooldown survives.
40
+ assert.equal(applyProviderCooldown(fastMode, { untilMs: now - 5_000 }, now), false);
41
+ assert.equal(applyProviderCooldown(fastMode, { untilMs: now + 500 }, now), false);
42
+ assert.equal(fastMode.fastModeCooldownRemainingMs(now), remaining);
43
+ assert.equal(fastMode.fastModeAvailable(now), false);
44
+ } finally {
45
+ fastMode.clearFastModeCooldown();
46
+ }
47
+ });
48
+
49
+ test('a disabled fast pool replays as a terminal disable', () => {
50
+ fastMode.clearFastModeCooldown();
51
+ try {
52
+ const now = Date.now();
53
+ assert.equal(
54
+ applyProviderCooldown(fastMode, { untilMs: 0, disabledReason: 'overage unavailable' }, now),
55
+ true,
56
+ );
57
+ assert.equal(fastMode.fastModeDisabledReason(), 'overage unavailable');
58
+ assert.equal(fastMode.fastModeAvailable(now), false);
59
+ const observed = readProviderCooldown(fastMode, now);
60
+ assert.equal(observed.disabledReason, 'overage unavailable');
61
+ } finally {
62
+ fastMode.clearFastModeCooldown();
63
+ }
64
+ });
65
+
66
+ test('replayed state is not echoed back as a new local discovery', () => {
67
+ const known = mergeKnownProviderCooldown(
68
+ { untilMs: 0, disabledReason: null },
69
+ { untilMs: 10_000, disabledReason: null },
70
+ );
71
+ assert.equal(known.untilMs, 10_000);
72
+ assert.equal(providerCooldownAdvanced(known, { untilMs: 10_000 }), false);
73
+ assert.equal(providerCooldownAdvanced(known, { untilMs: 10_500 }), false);
74
+ assert.equal(providerCooldownAdvanced(known, { untilMs: 30_000 }), true);
75
+ assert.equal(providerCooldownAdvanced(known, { untilMs: 0, disabledReason: 'off' }), true);
76
+ assert.equal(
77
+ providerCooldownAdvanced({ ...known, disabledReason: 'off' }, { untilMs: 0, disabledReason: 'off' }),
78
+ false,
79
+ );
80
+ });