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.
- package/README.md +110 -68
- package/package.json +3 -2
- package/scripts/tool-stress.mjs +0 -2
- package/src/defaults/skills/setup/SKILL.md +18 -27
- package/src/headless-exec.mjs +3 -10
- package/src/headless-exec.test.mjs +2 -7
- package/src/runtime/agent/orchestrator/config.mjs +6 -66
- package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
- package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
- package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
- package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
- package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
- package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/browser-bridge/client.mjs +103 -0
- package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
- package/src/runtime/computer-bridge/client.mjs +101 -0
- package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
- package/src/runtime/media/store.mjs +18 -8
- package/src/runtime/media/store.test.mjs +14 -0
- package/src/runtime/memory/index.mjs +1 -1
- package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
- package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
- package/src/runtime/shared/child-spawn-gate.mjs +17 -0
- package/src/runtime/shared/child-spawn-remote.mjs +69 -7
- package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
- package/src/runtime/shared/pristine-execution-contract.json +2 -1
- package/src/runtime/shared/provider-api-key.mjs +1 -0
- package/src/runtime/shared/session-runtime-health.mjs +91 -0
- package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
- package/src/runtime/shared/skill-document.mjs +86 -0
- package/src/runtime/shared/skill-document.test.mjs +81 -0
- package/src/runtime/shared/tool-surface.mjs +18 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
- package/src/runtime/shared/turn-snapshot.mjs +32 -9
- package/src/runtime/shared/user-cwd.mjs +33 -14
- package/src/runtime/shared/user-cwd.test.mjs +43 -0
- package/src/session-runtime/cwd-plugins.mjs +26 -26
- package/src/session-runtime/cwd-plugins.test.mjs +75 -0
- package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
- package/src/session-runtime/global-extensions.mjs +26 -0
- package/src/session-runtime/global-extensions.test.mjs +28 -0
- package/src/session-runtime/goal-runtime.mjs +721 -0
- package/src/session-runtime/goal-runtime.test.mjs +108 -0
- package/src/session-runtime/lifecycle-api.mjs +18 -4
- package/src/session-runtime/lifecycle-api.test.mjs +21 -0
- package/src/session-runtime/mcp-glue.mjs +49 -47
- package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
- package/src/session-runtime/plugin-mcp.mjs +96 -22
- package/src/session-runtime/prewarm.mjs +1 -10
- package/src/session-runtime/resource-api-global.test.mjs +69 -0
- package/src/session-runtime/resource-api.mjs +173 -58
- package/src/session-runtime/runtime-core.mjs +148 -20
- package/src/session-runtime/runtime-tunables.mjs +6 -8
- package/src/session-runtime/session-turn-api.mjs +22 -11
- package/src/session-runtime/skills-api.mjs +79 -25
- package/src/session-runtime/skills-api.test.mjs +99 -0
- package/src/session-runtime/tool-catalog-schema.mjs +0 -5
- package/src/session-runtime/tool-catalog.mjs +4 -13
- package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
- package/src/session-runtime/tool-surface.mjs +12 -0
- package/src/standalone/agent-dispatch-broker.mjs +5 -9
- package/src/standalone/agent-tool/notify.mjs +6 -8
- package/src/standalone/agent-tool/notify.test.mjs +37 -0
- package/src/standalone/daemon.mjs +31 -15
- package/src/standalone/hook-bus/config.mjs +1 -0
- package/src/standalone/plugin-admin.mjs +17 -0
- package/src/standalone/plugin-admin.test.mjs +7 -0
- package/src/standalone/provider-admin.mjs +1 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
- package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
- package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
- package/src/standalone/session-runtime-host-factory.mjs +17 -0
- package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
- package/src/standalone/session-runtime-host-health.test.mjs +5 -0
- package/src/standalone/session-runtime-host.mjs +696 -58
- package/src/standalone/session-runtime-inline-host.mjs +351 -0
- package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
- package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
- package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
- package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
- package/src/standalone/session-runtime-shard-router.mjs +149 -0
- package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
- package/src/standalone/session-runtime-worker.mjs +476 -18
- package/src/standalone/session-service.mjs +81 -6
- package/src/tui/app/app-view.jsx +1 -0
- package/src/tui/app/core-memory-picker.mjs +2 -0
- package/src/tui/app/extension-pickers.mjs +1 -1
- package/src/tui/app/maintenance-pickers.mjs +3 -3
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/model-picker.mjs +14 -4
- package/src/tui/app/panel-handoff.test.mjs +311 -0
- package/src/tui/app/project-picker.mjs +1 -0
- package/src/tui/app/route-pickers.mjs +21 -6
- package/src/tui/app/settings-picker.mjs +26 -3
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +73 -22
- package/src/tui/app/theme-effort-pickers.mjs +7 -2
- package/src/tui/components/Picker.jsx +10 -1
- package/src/tui/dist/index.mjs +163 -44
- package/src/tui/session/context-state.mjs +1 -0
- package/src/tui/session/goal-continuation.mjs +110 -0
- package/src/tui/session/goal-continuation.test.mjs +84 -0
- package/src/tui/session/queue-helpers.mjs +5 -2
- package/src/tui/session/session-action-surface.test.mjs +108 -0
- package/src/tui/session/session-api-ext.mjs +39 -8
- package/src/tui/session/session-api.mjs +58 -0
- package/src/tui/session/session-flow.mjs +6 -0
- package/src/tui/session/turn.mjs +1 -0
- package/src/tui/session-local.mjs +15 -1
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
|
@@ -12,11 +12,21 @@ import {
|
|
|
12
12
|
countTokensNative,
|
|
13
13
|
prewarmNativeTokenCounter,
|
|
14
14
|
} from '../runtime/agent/orchestrator/session/token-native.mjs';
|
|
15
|
+
import { createRuntimeLagTracker } from '../runtime/shared/session-runtime-health.mjs';
|
|
15
16
|
import {
|
|
16
17
|
SESSION_CONFIGURE_ACTIONS,
|
|
17
18
|
SESSION_READ_ACTIONS,
|
|
18
19
|
} from './session-protocol.mjs';
|
|
20
|
+
import {
|
|
21
|
+
createShardOwnership,
|
|
22
|
+
mergeProviderCooldown,
|
|
23
|
+
normalizeShardCount,
|
|
24
|
+
resolveShardCount,
|
|
25
|
+
runtimeRoutingKey,
|
|
26
|
+
selectShardIndex,
|
|
27
|
+
} from './session-runtime-shard-router.mjs';
|
|
19
28
|
import { applySessionStatePatch } from './session-state-patch.mjs';
|
|
29
|
+
import { renderResult as renderAgentResult } from './agent-tool/render.mjs';
|
|
20
30
|
|
|
21
31
|
const RUNTIME_METHODS = new Set([
|
|
22
32
|
...SESSION_READ_ACTIONS,
|
|
@@ -37,12 +47,24 @@ export function applyRuntimeStateFrame(previous, frame) {
|
|
|
37
47
|
return applySessionStatePatch(previous, patch);
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
/**
|
|
51
|
+
* One runtime child process = one shard = one event-loop failure domain.
|
|
52
|
+
*
|
|
53
|
+
* Everything below (identity, revisions, recovery, recycling, spawn leases)
|
|
54
|
+
* is scoped to THIS child, so a saturated or crashed shard can never freeze
|
|
55
|
+
* control/abort for sessions owned by a sibling shard.
|
|
56
|
+
*/
|
|
57
|
+
class SessionRuntimeShard {
|
|
58
|
+
constructor({ index = 0, pool = null, workerEntry, cwd, env, log, onAgentSessionState }) {
|
|
59
|
+
this.index = Math.max(0, Math.floor(Number(index) || 0));
|
|
60
|
+
this.pool = pool;
|
|
42
61
|
this.workerEntry = workerEntry;
|
|
43
62
|
this.cwd = cwd;
|
|
44
63
|
this.env = env;
|
|
45
|
-
|
|
64
|
+
const emit = typeof log === 'function' ? log : () => {};
|
|
65
|
+
this.log = (line) => emit(`session runtime shard ${this.index} ${line}`);
|
|
66
|
+
this.lag = createRuntimeLagTracker();
|
|
67
|
+
this.onAgentSessionState = onAgentSessionState;
|
|
46
68
|
this.child = null;
|
|
47
69
|
this.pending = new Map();
|
|
48
70
|
this.proxies = new Map();
|
|
@@ -50,6 +72,8 @@ class SessionRuntimeWorker {
|
|
|
50
72
|
this.closed = false;
|
|
51
73
|
this.recovery = null;
|
|
52
74
|
this.recycling = null;
|
|
75
|
+
this.prewarmChildRef = null;
|
|
76
|
+
this.prewarmPromise = null;
|
|
53
77
|
this.failedChildren = new WeakSet();
|
|
54
78
|
// Machine-wide spawn leases held on behalf of the runtime child process.
|
|
55
79
|
this.spawnLeases = new Map(); // leaseId -> { release, controller, settled }
|
|
@@ -66,9 +90,11 @@ class SessionRuntimeWorker {
|
|
|
66
90
|
...hiddenSpawnOpts,
|
|
67
91
|
});
|
|
68
92
|
this.child = child;
|
|
93
|
+
// Lag history belongs to the process that produced it.
|
|
94
|
+
this.lag.reset();
|
|
69
95
|
child.stderr?.on('data', (chunk) => {
|
|
70
96
|
const text = String(chunk || '').trimEnd();
|
|
71
|
-
if (text) this.log(`
|
|
97
|
+
if (text) this.log(`stderr: ${text}`);
|
|
72
98
|
});
|
|
73
99
|
child.on('message', (message) => this.onMessage(message, child));
|
|
74
100
|
child.on('error', (error) => this.handleChildFailure(child, error));
|
|
@@ -79,6 +105,9 @@ class SessionRuntimeWorker {
|
|
|
79
105
|
new Error(`session runtime worker exited (${signal || code || 'unknown'})`),
|
|
80
106
|
);
|
|
81
107
|
});
|
|
108
|
+
// Account-wide provider cooldown and prewarm state are pool facts, not
|
|
109
|
+
// child facts: a freshly forked shard inherits both immediately.
|
|
110
|
+
this.pool?.primeChild(this);
|
|
82
111
|
return child;
|
|
83
112
|
}
|
|
84
113
|
|
|
@@ -104,10 +133,38 @@ class SessionRuntimeWorker {
|
|
|
104
133
|
this.settleSpawnLease(String(message.leaseId || ''));
|
|
105
134
|
return;
|
|
106
135
|
}
|
|
136
|
+
if (message.type === 'event-loop-lag') {
|
|
137
|
+
this.recordLag(message.sample);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (message.type === 'provider-cooldown') {
|
|
141
|
+
this.pool?.recordProviderCooldown(this, message);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (message.type === 'agent-control') {
|
|
145
|
+
void this.pool?.handleAgentControl(this, child, message);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (message.type === 'agent-control-cancel') {
|
|
149
|
+
this.pool?.cancelAgentControl(message);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (message.type === 'agent-control-notification') {
|
|
153
|
+
this.pool?.routeAgentControlNotification(message);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
107
156
|
if (message.type === 'unhealthy') {
|
|
108
157
|
this.recycleUnhealthy(child, message.detail);
|
|
109
158
|
return;
|
|
110
159
|
}
|
|
160
|
+
if (message.type === 'agent-session-state') {
|
|
161
|
+
const sessionId = String(message.sessionId || '');
|
|
162
|
+
const snapshot = message.snapshot;
|
|
163
|
+
if (sessionId && snapshot && typeof snapshot === 'object') {
|
|
164
|
+
this.onAgentSessionState?.({ sessionId, snapshot });
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
111
168
|
if (message.type === 'state') {
|
|
112
169
|
this.proxies.get(String(message.runtimeId || ''))?.applyFrame(message);
|
|
113
170
|
return;
|
|
@@ -169,7 +226,7 @@ class SessionRuntimeWorker {
|
|
|
169
226
|
// Same identity rule for failures: a retired child's late exit/error must
|
|
170
227
|
// not reject the replacement's in-flight calls or start a second recovery.
|
|
171
228
|
if (this.child && this.child !== child) {
|
|
172
|
-
this.log(`
|
|
229
|
+
this.log(`superseded child failed (ignored): ${error?.message || error}`);
|
|
173
230
|
return;
|
|
174
231
|
}
|
|
175
232
|
if (this.child === child) this.child = null;
|
|
@@ -190,7 +247,7 @@ class SessionRuntimeWorker {
|
|
|
190
247
|
try {
|
|
191
248
|
this.ensureChild();
|
|
192
249
|
for (const proxy of this.proxies.values()) await proxy.recreate();
|
|
193
|
-
this.log(`
|
|
250
|
+
this.log(`recovered ${this.proxies.size} runtime(s)`);
|
|
194
251
|
return true;
|
|
195
252
|
} catch (error) {
|
|
196
253
|
lastError = error;
|
|
@@ -209,11 +266,11 @@ class SessionRuntimeWorker {
|
|
|
209
266
|
|
|
210
267
|
recycleChild(child, reason) {
|
|
211
268
|
if (this.closed || child !== this.child || this.recycling) return;
|
|
212
|
-
this.log(`
|
|
269
|
+
this.log(`recycling pid=${child.pid || 'unknown'}: ${reason}`);
|
|
213
270
|
this.recycling = this.stopChild(reason)
|
|
214
271
|
.then(() => true)
|
|
215
272
|
.catch((error) => {
|
|
216
|
-
this.log(`
|
|
273
|
+
this.log(`recycle failed: ${error?.message || error}`);
|
|
217
274
|
try { child.kill?.(); } catch {}
|
|
218
275
|
return false;
|
|
219
276
|
})
|
|
@@ -259,19 +316,106 @@ class SessionRuntimeWorker {
|
|
|
259
316
|
return this.requestRaw(type, payload, timeoutMs);
|
|
260
317
|
}
|
|
261
318
|
|
|
262
|
-
async create(options) {
|
|
263
|
-
const
|
|
264
|
-
const proxy = new SessionRuntimeProxy(runtimeId, this, options);
|
|
319
|
+
async create(runtimeId, options, routingKey = '') {
|
|
320
|
+
const proxy = new SessionRuntimeProxy(runtimeId, this, options, routingKey);
|
|
265
321
|
this.proxies.set(runtimeId, proxy);
|
|
266
322
|
try {
|
|
267
323
|
await this.request('create', { runtimeId, options }, 120_000);
|
|
268
324
|
return proxy;
|
|
269
325
|
} catch (error) {
|
|
326
|
+
// Drop the proxy only. The ownership claim belongs to whoever CLAIMED it
|
|
327
|
+
// (the pool's create path) and is released there exactly once: releasing
|
|
328
|
+
// it here too would decrement a claim still held by another live view of
|
|
329
|
+
// the same session and split that session across two shards after a
|
|
330
|
+
// later quarantine.
|
|
270
331
|
this.proxies.delete(runtimeId);
|
|
271
332
|
throw error;
|
|
272
333
|
}
|
|
273
334
|
}
|
|
274
335
|
|
|
336
|
+
/** Drop a runtime AND its shard ownership claim in one place. */
|
|
337
|
+
releaseProxy(runtimeId) {
|
|
338
|
+
const id = String(runtimeId || '');
|
|
339
|
+
const proxy = this.proxies.get(id);
|
|
340
|
+
if (!proxy) return;
|
|
341
|
+
this.proxies.delete(id);
|
|
342
|
+
if (proxy.ownershipReleased) return;
|
|
343
|
+
proxy.ownershipReleased = true;
|
|
344
|
+
this.pool?.releaseOwnership(proxy.routingKey);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Event-loop lag is a routing signal, never a kill signal: a saturated
|
|
348
|
+
* shard still owns accepted input and in-flight turns. */
|
|
349
|
+
recordLag(sample) {
|
|
350
|
+
const row = sample && typeof sample === 'object' ? sample : null;
|
|
351
|
+
const result = this.lag.record(row);
|
|
352
|
+
const detail = `p95=${row?.p95Ms ?? -1}ms p99=${row?.p99Ms ?? -1}ms max=${row?.maxMs ?? -1}ms`
|
|
353
|
+
+ ` runtimes=${this.proxies.size}`;
|
|
354
|
+
if (result.changed) {
|
|
355
|
+
this.log(result.degraded
|
|
356
|
+
? `event-loop saturated ${detail} — quarantined from new placement`
|
|
357
|
+
: `event-loop lag recovered ${detail} — placement resumed`);
|
|
358
|
+
} else if (Number(row?.p99Ms) >= Number(this.lag.config.warnP99Ms)) {
|
|
359
|
+
this.log(`event-loop lag ${detail}`);
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Prewarm is per CHILD and issued exactly once for it: the spawn path and
|
|
365
|
+
* an explicit host prewarm share the same in-flight promise instead of
|
|
366
|
+
* racing two module-preload passes into one fresh process. */
|
|
367
|
+
prewarmChild() {
|
|
368
|
+
const child = this.ensureChild();
|
|
369
|
+
if (this.prewarmChildRef === child && this.prewarmPromise) return this.prewarmPromise;
|
|
370
|
+
this.prewarmChildRef = child;
|
|
371
|
+
this.prewarmPromise = this.requestRaw('prewarm', {}, 120_000);
|
|
372
|
+
return this.prewarmPromise;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
sendProviderCooldown(cooldown, admission = null) {
|
|
376
|
+
const child = this.child;
|
|
377
|
+
if (!child || child.killed || this.closed) return false;
|
|
378
|
+
return safeIpcSend(child, {
|
|
379
|
+
type: 'provider-cooldown-sync',
|
|
380
|
+
cooldown,
|
|
381
|
+
...(admission ? { admission } : {}),
|
|
382
|
+
}, { onError: () => {} });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
sendAgentControlNotification(message) {
|
|
386
|
+
const child = this.child;
|
|
387
|
+
if (!child || child.killed || this.closed) return false;
|
|
388
|
+
return safeIpcSend(child, {
|
|
389
|
+
type: 'agent-control-notification',
|
|
390
|
+
ownerSessionId: String(message?.ownerSessionId || ''),
|
|
391
|
+
text: String(message?.text || ''),
|
|
392
|
+
meta: message?.meta && typeof message.meta === 'object' ? message.meta : {},
|
|
393
|
+
}, { onError: () => {} });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async workloadSnapshot() {
|
|
397
|
+
const base = {
|
|
398
|
+
shard: this.index,
|
|
399
|
+
pid: this.child?.pid || null,
|
|
400
|
+
degraded: this.lag.degraded,
|
|
401
|
+
eventLoopLag: this.lag.sample,
|
|
402
|
+
};
|
|
403
|
+
try {
|
|
404
|
+
const value = await this.request('workload', {}, 2_000);
|
|
405
|
+
return { ...base, pid: this.child?.pid || base.pid, ...value };
|
|
406
|
+
} catch (error) {
|
|
407
|
+
return { ...base, error: String(error?.message || error) };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
get isLive() {
|
|
412
|
+
return Boolean(this.child) && !this.child.killed;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
get degraded() {
|
|
416
|
+
return this.lag.degraded;
|
|
417
|
+
}
|
|
418
|
+
|
|
275
419
|
/** Grant one machine-wide spawn lease from the daemon-side gate (the single
|
|
276
420
|
* budget authority; daemon-hosted work uses the same instance locally). */
|
|
277
421
|
async grantSpawnLease(message) {
|
|
@@ -366,14 +510,96 @@ class SessionRuntimeWorker {
|
|
|
366
510
|
|
|
367
511
|
get status() {
|
|
368
512
|
return {
|
|
513
|
+
index: this.index,
|
|
369
514
|
pid: this.child?.pid || null,
|
|
370
515
|
runtimes: this.proxies.size,
|
|
371
516
|
pending: this.pending.size,
|
|
372
517
|
recycling: Boolean(this.recycling),
|
|
518
|
+
degraded: this.lag.degraded,
|
|
519
|
+
lag: this.lag.sample,
|
|
373
520
|
};
|
|
374
521
|
}
|
|
375
522
|
}
|
|
376
523
|
|
|
524
|
+
// Fields whose machine-wide value is the WORST shard rather than the sum:
|
|
525
|
+
// configured caps do not add up across processes, and wait/age gauges are
|
|
526
|
+
// worst-case signals. Everything else (inflight, queued, counters, bytes) is
|
|
527
|
+
// real per-process load and sums.
|
|
528
|
+
const WORKLOAD_MAX_FIELDS = new Set([
|
|
529
|
+
'limit', 'maxInflight', 'queueMax', 'activeMax', 'waitTimeoutMs', 'concurrency',
|
|
530
|
+
'maxAgents', 'maxShells', 'maxHighLoad', 'maxQueue', 'minFreeMemoryMb', 'maxRssMb',
|
|
531
|
+
'oldestWaitMs', 'oldestQueuedMs', 'maxWaitMs', 'averageWaitMs', 'ageMs',
|
|
532
|
+
'freeMemoryBytes', 'totalMemoryBytes',
|
|
533
|
+
'p50Ms', 'p95Ms', 'p99Ms', 'maxMs', 'meanMs', 'intervalMs', 'at',
|
|
534
|
+
]);
|
|
535
|
+
const WORKLOAD_LIST_CAP = 64;
|
|
536
|
+
|
|
537
|
+
/** Structural merge of two shard workload rows (exported for tests). */
|
|
538
|
+
export function mergeShardWorkloadValues(left, right, key = '') {
|
|
539
|
+
if (right === undefined || right === null) return left;
|
|
540
|
+
if (left === undefined || left === null) return right;
|
|
541
|
+
if (typeof left === 'number' && typeof right === 'number') {
|
|
542
|
+
return WORKLOAD_MAX_FIELDS.has(key) ? Math.max(left, right) : left + right;
|
|
543
|
+
}
|
|
544
|
+
if (typeof left === 'boolean' || typeof right === 'boolean') {
|
|
545
|
+
return Boolean(left) || Boolean(right);
|
|
546
|
+
}
|
|
547
|
+
if (typeof left === 'string' || typeof right === 'string') {
|
|
548
|
+
return left === right ? left : 'mixed';
|
|
549
|
+
}
|
|
550
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
551
|
+
const entries = [...left, ...right];
|
|
552
|
+
const named = entries.length > 0
|
|
553
|
+
&& entries.every((entry) => entry && typeof entry === 'object' && typeof entry.name === 'string');
|
|
554
|
+
if (!named) return entries.slice(0, WORKLOAD_LIST_CAP);
|
|
555
|
+
// Lane/gate rows are per-process views of ONE machine-wide lane: merge by
|
|
556
|
+
// name so `childSpawns.lanes[].inflight` reports the machine total.
|
|
557
|
+
const byName = new Map();
|
|
558
|
+
for (const entry of entries) {
|
|
559
|
+
const existing = byName.get(entry.name);
|
|
560
|
+
byName.set(entry.name, existing ? mergeShardWorkloadValues(existing, entry) : entry);
|
|
561
|
+
}
|
|
562
|
+
return [...byName.values()];
|
|
563
|
+
}
|
|
564
|
+
if (typeof left === 'object' && typeof right === 'object') {
|
|
565
|
+
const out = {};
|
|
566
|
+
for (const field of new Set([...Object.keys(left), ...Object.keys(right)])) {
|
|
567
|
+
out[field] = mergeShardWorkloadValues(left[field], right[field], field);
|
|
568
|
+
}
|
|
569
|
+
return out;
|
|
570
|
+
}
|
|
571
|
+
return left;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** Aggregate EVERY live shard into the single-worker shape older status
|
|
575
|
+
* consumers still read (resources/toolIo/childSpawns included), keeping the
|
|
576
|
+
* per-shard rows alongside it. */
|
|
577
|
+
export function aggregateShardWorkload(rows) {
|
|
578
|
+
const list = Array.isArray(rows) ? rows.filter(Boolean) : [];
|
|
579
|
+
if (list.length === 0) return null;
|
|
580
|
+
let merged = {};
|
|
581
|
+
for (const row of list) merged = mergeShardWorkloadValues(merged, row);
|
|
582
|
+
const errors = list
|
|
583
|
+
.filter((row) => row?.error)
|
|
584
|
+
.map((row) => `shard ${row.shard ?? '?'}: ${row.error}`);
|
|
585
|
+
const worstLag = list.reduce((worst, row) => {
|
|
586
|
+
const sample = row?.eventLoopLag;
|
|
587
|
+
if (!sample) return worst;
|
|
588
|
+
return !worst || (Number(sample.p99Ms) || 0) > (Number(worst.p99Ms) || 0) ? sample : worst;
|
|
589
|
+
}, null);
|
|
590
|
+
return {
|
|
591
|
+
...merged,
|
|
592
|
+
// Identity/telemetry fields describe shards individually — never summed.
|
|
593
|
+
shard: list[0]?.shard ?? 0,
|
|
594
|
+
pid: list.find((row) => row?.pid)?.pid ?? null,
|
|
595
|
+
pids: list.map((row) => row?.pid ?? null),
|
|
596
|
+
shards: list.length,
|
|
597
|
+
degraded: list.some((row) => row?.degraded === true),
|
|
598
|
+
eventLoopLag: worstLag,
|
|
599
|
+
...(errors.length > 0 ? { error: errors[0], errors } : {}),
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
377
603
|
// Wire contract for runtime calls: the fork IPC link serializes frames
|
|
378
604
|
// as JSON, which cannot represent `undefined` and would fabricate `null` in
|
|
379
605
|
// its place — turning an omitted optional argument (e.g. `resume(id)`) into a
|
|
@@ -389,9 +615,14 @@ function wireCallArgs(args) {
|
|
|
389
615
|
}
|
|
390
616
|
|
|
391
617
|
class SessionRuntimeProxy {
|
|
392
|
-
constructor(id,
|
|
618
|
+
constructor(id, shard, options, routingKey = '') {
|
|
393
619
|
this.id = id;
|
|
394
|
-
this.
|
|
620
|
+
this.shard = shard;
|
|
621
|
+
// Stable ownership key (sessionId when known): recovery, resume and every
|
|
622
|
+
// later view of this session resolve to the same shard. The claim is
|
|
623
|
+
// released exactly once, by whichever settle path runs first.
|
|
624
|
+
this.routingKey = String(routingKey || id);
|
|
625
|
+
this.ownershipReleased = false;
|
|
395
626
|
this.options = { ...(options || {}) };
|
|
396
627
|
this.state = {};
|
|
397
628
|
this.revision = 0;
|
|
@@ -417,7 +648,7 @@ class SessionRuntimeProxy {
|
|
|
417
648
|
const revision = Number(frame.revision) || 0;
|
|
418
649
|
if (revision <= this.revision) return;
|
|
419
650
|
if (frame.patch && revision !== this.revision + 1) {
|
|
420
|
-
void this.
|
|
651
|
+
void this.shard.request('snapshot', { runtimeId: this.id }, 5_000).catch(() => {});
|
|
421
652
|
return;
|
|
422
653
|
}
|
|
423
654
|
this.state = applyRuntimeStateFrame(this.state, frame);
|
|
@@ -445,45 +676,414 @@ class SessionRuntimeProxy {
|
|
|
445
676
|
async recreate() {
|
|
446
677
|
const previousSessionId = String(this.state?.sessionId || this.options.sessionId || '');
|
|
447
678
|
this.revision = 0;
|
|
448
|
-
await this.
|
|
679
|
+
await this.shard.requestRaw('create', {
|
|
449
680
|
runtimeId: this.id,
|
|
450
681
|
options: this.options,
|
|
451
682
|
}, 120_000);
|
|
452
683
|
if (previousSessionId) {
|
|
453
|
-
const resumed = await this.
|
|
684
|
+
const resumed = await this.shard.requestRaw('call', {
|
|
454
685
|
runtimeId: this.id,
|
|
455
686
|
method: 'resume',
|
|
456
687
|
args: [previousSessionId],
|
|
457
688
|
}, 120_000);
|
|
458
689
|
if (resumed !== true) {
|
|
459
|
-
await this.
|
|
690
|
+
await this.shard.requestRaw('call', {
|
|
460
691
|
runtimeId: this.id,
|
|
461
692
|
method: 'reserveSession',
|
|
462
693
|
args: [previousSessionId],
|
|
463
694
|
}, 30_000);
|
|
464
695
|
}
|
|
465
696
|
}
|
|
466
|
-
await this.
|
|
697
|
+
await this.shard.requestRaw('snapshot', { runtimeId: this.id }, 5_000);
|
|
467
698
|
this.failure = null;
|
|
468
699
|
this.recovering = false;
|
|
469
700
|
}
|
|
470
701
|
|
|
471
702
|
async call(method, args) {
|
|
472
|
-
if (this.recovering && this.
|
|
703
|
+
if (this.recovering && this.shard.recovery) await this.shard.recovery;
|
|
473
704
|
if (this.failure) throw this.failure;
|
|
474
705
|
try {
|
|
475
|
-
const value = await this.
|
|
706
|
+
const value = await this.shard.request('call', {
|
|
476
707
|
runtimeId: this.id,
|
|
477
708
|
method,
|
|
478
709
|
args: wireCallArgs(args),
|
|
479
710
|
}, method === 'abort' ? 15_000 : 10 * 60_000);
|
|
480
|
-
if (method === 'dispose') this.
|
|
711
|
+
if (method === 'dispose') this.shard.releaseProxy(this.id);
|
|
481
712
|
return value;
|
|
482
713
|
} catch (error) {
|
|
483
|
-
if (method === 'dispose') this.
|
|
714
|
+
if (method === 'dispose') this.shard.releaseProxy(this.id);
|
|
715
|
+
throw error;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Bounded multi-child shard pool.
|
|
722
|
+
*
|
|
723
|
+
* Fan-out is unchanged — every shard runs the same full agent/session graph —
|
|
724
|
+
* but independent work is distributed so that no single event loop carries all
|
|
725
|
+
* sessions, all provider parsing and all tool results. Machine-wide
|
|
726
|
+
* authorities are deliberately NOT duplicated: child-spawn leases still flow
|
|
727
|
+
* through each shard's IPC channel to the one daemon-side gate, and provider
|
|
728
|
+
* capacity cooldowns discovered by one shard are replayed into the others.
|
|
729
|
+
*/
|
|
730
|
+
class SessionRuntimeShardPool {
|
|
731
|
+
constructor({ workerEntry, cwd, env, log, shardCount, onAgentSessionState }) {
|
|
732
|
+
this.log = typeof log === 'function' ? log : () => {};
|
|
733
|
+
this.closed = false;
|
|
734
|
+
this.ownership = createShardOwnership();
|
|
735
|
+
this.dispatchOwners = new Map(); // dispatchId -> shard index
|
|
736
|
+
this.agentControlOwners = new Map(); // controlId -> shard index
|
|
737
|
+
this.agentTaskOwners = new Map(); // taskId -> shard index
|
|
738
|
+
this.agentTagOwners = new Map(); // ownerSessionId\0tag -> shard index
|
|
739
|
+
this.agentSessionOwners = new Map(); // agent sessionId -> shard index
|
|
740
|
+
this.agentOwnerOrigins = new Map(); // owner sessionId -> source shard index
|
|
741
|
+
this.providerCooldown = { untilMs: 0, disabledReason: null, updatedAt: 0 };
|
|
742
|
+
this.prewarmRequested = false;
|
|
743
|
+
this.workloadCache = { refreshedAt: 0, shards: [] };
|
|
744
|
+
this.workloadRefresh = null;
|
|
745
|
+
// Hard bound, always: an explicit shardCount is operator/test input and
|
|
746
|
+
// must never fork an unbounded number of runtime children.
|
|
747
|
+
const count = normalizeShardCount(shardCount);
|
|
748
|
+
this.shards = Array.from({ length: count }, (_, index) => new SessionRuntimeShard({
|
|
749
|
+
index,
|
|
750
|
+
pool: this,
|
|
751
|
+
workerEntry,
|
|
752
|
+
cwd,
|
|
753
|
+
env: {
|
|
754
|
+
...env,
|
|
755
|
+
MIXDOG_SESSION_RUNTIME_SHARD: String(index),
|
|
756
|
+
MIXDOG_SESSION_RUNTIME_SHARD_COUNT: String(count),
|
|
757
|
+
},
|
|
758
|
+
log: this.log,
|
|
759
|
+
onAgentSessionState,
|
|
760
|
+
}));
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
get shardCount() {
|
|
764
|
+
return this.shards.length;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
shardAt(index) {
|
|
768
|
+
const count = this.shards.length;
|
|
769
|
+
const safe = ((Math.floor(Number(index) || 0) % count) + count) % count;
|
|
770
|
+
return this.shards[safe];
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
liveShards() {
|
|
774
|
+
return this.shards.filter((shard) => shard.isLive);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/** NEW work avoids a quarantined shard; work already owned never migrates
|
|
778
|
+
* (migration would strand accepted input in the abandoned child). */
|
|
779
|
+
isPlaceable(index) {
|
|
780
|
+
const shard = this.shards[index];
|
|
781
|
+
return Boolean(shard) && !shard.degraded && !shard.closed;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
placeKey(key) {
|
|
785
|
+
return this.ownership.claim(
|
|
786
|
+
key,
|
|
787
|
+
() => selectShardIndex(key, this.shards.length, (index) => this.isPlaceable(index)),
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
releaseOwnership(key) {
|
|
792
|
+
if (!key) return;
|
|
793
|
+
this.ownership.release(key);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async create(options = {}) {
|
|
797
|
+
const runtimeId = randomUUID();
|
|
798
|
+
const key = runtimeRoutingKey(options, runtimeId);
|
|
799
|
+
const shard = this.shardAt(this.placeKey(key));
|
|
800
|
+
try {
|
|
801
|
+
return await shard.create(runtimeId, options, key);
|
|
802
|
+
} catch (error) {
|
|
803
|
+
this.releaseOwnership(key);
|
|
484
804
|
throw error;
|
|
485
805
|
}
|
|
486
806
|
}
|
|
807
|
+
|
|
808
|
+
/** Agent dispatches are independent units of work: hash placement spreads
|
|
809
|
+
* a batch fan-out across shards, and the recorded owner keeps cancel/abort
|
|
810
|
+
* addressed to the shard actually running it. */
|
|
811
|
+
dispatchShard(dispatchId, { create = false } = {}) {
|
|
812
|
+
const id = String(dispatchId || '');
|
|
813
|
+
if (this.dispatchOwners.has(id)) return this.shardAt(this.dispatchOwners.get(id));
|
|
814
|
+
if (!create) return null;
|
|
815
|
+
const index = selectShardIndex(
|
|
816
|
+
`dispatch:${id}`,
|
|
817
|
+
this.shards.length,
|
|
818
|
+
(candidate) => this.isPlaceable(candidate),
|
|
819
|
+
);
|
|
820
|
+
this.dispatchOwners.set(id, index);
|
|
821
|
+
return this.shardAt(index);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
releaseDispatch(dispatchId) {
|
|
825
|
+
this.dispatchOwners.delete(String(dispatchId || ''));
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
agentTagKey(ownerSessionId, tag) {
|
|
829
|
+
return `${String(ownerSessionId || '')}\0${String(tag || '')}`;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
rememberAgentControlResult(index, args, context, value) {
|
|
833
|
+
const text = String(value || '');
|
|
834
|
+
const ownerSessionId = String(context?.callerSessionId || '');
|
|
835
|
+
const taskId = String(
|
|
836
|
+
args?.task_id
|
|
837
|
+
|| args?.taskId
|
|
838
|
+
|| (/^agent task:\s*(\S+)/m.exec(text)?.[1] || ''),
|
|
839
|
+
);
|
|
840
|
+
const target = /^target:\s*(\S+)(?:\s+(\S+))?/m.exec(text);
|
|
841
|
+
const tag = String(args?.tag || (target?.[1] && target[1] !== '-' ? target[1] : ''));
|
|
842
|
+
const sessionId = String(
|
|
843
|
+
args?.sessionId
|
|
844
|
+
|| args?.session_id
|
|
845
|
+
|| (target?.[2] && /^sess_/.test(target[2]) ? target[2] : ''),
|
|
846
|
+
);
|
|
847
|
+
if (taskId) this.agentTaskOwners.set(taskId, index);
|
|
848
|
+
if (tag) this.agentTagOwners.set(this.agentTagKey(ownerSessionId, tag), index);
|
|
849
|
+
if (sessionId) this.agentSessionOwners.set(sessionId, index);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
agentControlShard(originShard, args, context, controlId) {
|
|
853
|
+
const ownerSessionId = String(context?.callerSessionId || '');
|
|
854
|
+
const taskId = String(args?.task_id || args?.taskId || '');
|
|
855
|
+
const sessionId = String(args?.sessionId || args?.session_id || '');
|
|
856
|
+
const tag = String(args?.tag || '');
|
|
857
|
+
let index = taskId ? this.agentTaskOwners.get(taskId) : null;
|
|
858
|
+
if (index == null && sessionId) index = this.agentSessionOwners.get(sessionId);
|
|
859
|
+
if (index == null && tag) index = this.agentTagOwners.get(this.agentTagKey(ownerSessionId, tag));
|
|
860
|
+
if (index == null) {
|
|
861
|
+
const key = `agent:${ownerSessionId}:${tag || sessionId || controlId}`;
|
|
862
|
+
index = selectShardIndex(
|
|
863
|
+
key,
|
|
864
|
+
this.shards.length,
|
|
865
|
+
(candidate) => this.isPlaceable(candidate),
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
return this.shardAt(index ?? originShard?.index ?? 0);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async aggregateAgentStatus(context, originShard) {
|
|
872
|
+
const targets = this.liveShards();
|
|
873
|
+
if (targets.length === 0 && originShard) targets.push(originShard);
|
|
874
|
+
const settled = await Promise.allSettled(targets.map((shard) =>
|
|
875
|
+
shard.request('agent-control-status', { context }, 30_000)));
|
|
876
|
+
const workers = new Map();
|
|
877
|
+
const jobs = new Map();
|
|
878
|
+
for (const result of settled) {
|
|
879
|
+
if (result.status !== 'fulfilled') continue;
|
|
880
|
+
for (const worker of Array.isArray(result.value?.workers) ? result.value.workers : []) {
|
|
881
|
+
const key = String(worker?.sessionId || worker?.tag || '');
|
|
882
|
+
if (key) workers.set(key, worker);
|
|
883
|
+
}
|
|
884
|
+
for (const job of Array.isArray(result.value?.jobs) ? result.value.jobs : []) {
|
|
885
|
+
const key = String(job?.task_id || job?.taskId || '');
|
|
886
|
+
if (key) jobs.set(key, job);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return renderAgentResult({ workers: [...workers.values()], jobs: [...jobs.values()] });
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
async executeAgentControl(originShard, args = {}, context = {}, controlId = randomUUID()) {
|
|
893
|
+
const type = String(args?.type || 'spawn').trim().toLowerCase();
|
|
894
|
+
const ownerSessionId = String(context?.callerSessionId || '');
|
|
895
|
+
if (ownerSessionId && originShard) {
|
|
896
|
+
this.agentOwnerOrigins.set(ownerSessionId, originShard.index);
|
|
897
|
+
}
|
|
898
|
+
if (type === 'list') return this.aggregateAgentStatus(context, originShard);
|
|
899
|
+
if (type === 'cleanup' || type === '__close_all') {
|
|
900
|
+
const targets = this.liveShards();
|
|
901
|
+
const settled = await Promise.allSettled(targets.map((shard) =>
|
|
902
|
+
shard.request('agent-control-local', {
|
|
903
|
+
controlId: `${controlId}:${shard.index}`,
|
|
904
|
+
args,
|
|
905
|
+
context,
|
|
906
|
+
}, 180_000)));
|
|
907
|
+
return settled
|
|
908
|
+
.filter((result) => result.status === 'fulfilled' && result.value)
|
|
909
|
+
.map((result) => String(result.value))
|
|
910
|
+
.join('\n');
|
|
911
|
+
}
|
|
912
|
+
const target = this.agentControlShard(originShard, args, context, controlId);
|
|
913
|
+
this.agentControlOwners.set(controlId, target.index);
|
|
914
|
+
try {
|
|
915
|
+
let value = await target.request('agent-control-local', {
|
|
916
|
+
controlId,
|
|
917
|
+
args,
|
|
918
|
+
context,
|
|
919
|
+
}, 180_000);
|
|
920
|
+
const lookup = type === 'status' || type === 'read' || type === 'cancel' || type === 'close';
|
|
921
|
+
if (lookup && /^Error:/i.test(String(value || ''))) {
|
|
922
|
+
for (const alternate of this.liveShards()) {
|
|
923
|
+
if (alternate === target) continue;
|
|
924
|
+
const candidate = await alternate.request('agent-control-local', {
|
|
925
|
+
controlId: `${controlId}:${alternate.index}`,
|
|
926
|
+
args,
|
|
927
|
+
context,
|
|
928
|
+
}, 180_000);
|
|
929
|
+
if (/^Error:/i.test(String(candidate || ''))) continue;
|
|
930
|
+
value = candidate;
|
|
931
|
+
this.rememberAgentControlResult(alternate.index, args, context, value);
|
|
932
|
+
return value;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
this.rememberAgentControlResult(target.index, args, context, value);
|
|
936
|
+
return value;
|
|
937
|
+
} finally {
|
|
938
|
+
this.agentControlOwners.delete(controlId);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
handleAgentControl(originShard, originChild, message) {
|
|
943
|
+
const controlId = String(message?.controlId || '');
|
|
944
|
+
if (!controlId) return;
|
|
945
|
+
void this.executeAgentControl(
|
|
946
|
+
originShard,
|
|
947
|
+
message.args || {},
|
|
948
|
+
message.context || {},
|
|
949
|
+
controlId,
|
|
950
|
+
).then((value) => {
|
|
951
|
+
if (originShard.child !== originChild || originChild?.killed) return;
|
|
952
|
+
safeIpcSend(originChild, {
|
|
953
|
+
type: 'agent-control-result',
|
|
954
|
+
controlId,
|
|
955
|
+
ok: true,
|
|
956
|
+
value,
|
|
957
|
+
}, { onError: () => {} });
|
|
958
|
+
}).catch((error) => {
|
|
959
|
+
if (originShard.child !== originChild || originChild?.killed) return;
|
|
960
|
+
safeIpcSend(originChild, {
|
|
961
|
+
type: 'agent-control-result',
|
|
962
|
+
controlId,
|
|
963
|
+
ok: false,
|
|
964
|
+
error: {
|
|
965
|
+
name: String(error?.name || 'Error'),
|
|
966
|
+
message: String(error?.message || error || 'agent control failed'),
|
|
967
|
+
stack: typeof error?.stack === 'string' ? error.stack : null,
|
|
968
|
+
code: error?.code || null,
|
|
969
|
+
},
|
|
970
|
+
}, { onError: () => {} });
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
cancelAgentControl(message) {
|
|
975
|
+
const controlId = String(message?.controlId || '');
|
|
976
|
+
const index = this.agentControlOwners.get(controlId);
|
|
977
|
+
if (index == null) return false;
|
|
978
|
+
const shard = this.shardAt(index);
|
|
979
|
+
void shard.request('agent-control-local-cancel', {
|
|
980
|
+
controlId,
|
|
981
|
+
reason: String(message?.reason || 'agent control canceled'),
|
|
982
|
+
}, 5_000).catch(() => {});
|
|
983
|
+
return true;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
routeAgentControlNotification(message) {
|
|
987
|
+
const ownerSessionId = String(message?.ownerSessionId || '');
|
|
988
|
+
if (!ownerSessionId) return false;
|
|
989
|
+
const index = this.agentOwnerOrigins.get(ownerSessionId)
|
|
990
|
+
?? this.ownership.peek(ownerSessionId);
|
|
991
|
+
if (index == null) return false;
|
|
992
|
+
return this.shardAt(index).sendAgentControlNotification(message);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
async prewarm() {
|
|
996
|
+
this.prewarmRequested = true;
|
|
997
|
+
// Shard 0 always exists: its readiness is the daemon's prewarm contract.
|
|
998
|
+
// Shards that have not forked yet stay cold (no idle worker per core) and
|
|
999
|
+
// prewarm themselves the moment they spawn — see primeChild.
|
|
1000
|
+
const targets = new Set([this.shardAt(0), ...this.liveShards()]);
|
|
1001
|
+
const results = await Promise.allSettled(
|
|
1002
|
+
[...targets].map((shard) => shard.prewarmChild()),
|
|
1003
|
+
);
|
|
1004
|
+
const ready = results.find((result) => result.status === 'fulfilled');
|
|
1005
|
+
if (ready) return ready.value;
|
|
1006
|
+
const failed = results.find((result) => result.status === 'rejected');
|
|
1007
|
+
if (failed) throw failed.reason;
|
|
1008
|
+
return { ready: true };
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
primeChild(shard) {
|
|
1012
|
+
const cooldown = this.providerCooldown;
|
|
1013
|
+
if (cooldown.disabledReason || cooldown.untilMs > Date.now()) {
|
|
1014
|
+
shard.sendProviderCooldown(cooldown);
|
|
1015
|
+
}
|
|
1016
|
+
if (this.prewarmRequested && !this.closed) {
|
|
1017
|
+
// A shard that forks after the daemon's prewarm warms itself, so a
|
|
1018
|
+
// lazily spawned shard is never the cold one in a fan-out burst.
|
|
1019
|
+
void shard.prewarmChild().catch(() => {});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/** Provider capacity cooldown is account-wide but observed per process:
|
|
1024
|
+
* merge monotonically and replay to every sibling shard so a drained fast
|
|
1025
|
+
* pool is never re-probed by the shards that did not see the rejection. */
|
|
1026
|
+
recordProviderCooldown(originShard, payload) {
|
|
1027
|
+
const admission = payload?.admission && typeof payload.admission === 'object'
|
|
1028
|
+
? payload.admission
|
|
1029
|
+
: null;
|
|
1030
|
+
const merged = mergeProviderCooldown(this.providerCooldown, payload?.cooldown);
|
|
1031
|
+
if (merged.changed) this.providerCooldown = merged.cooldown;
|
|
1032
|
+
// An admission cooldown/reset event is forwarded the moment it happens —
|
|
1033
|
+
// siblings must stop probing a drained account immediately, not after the
|
|
1034
|
+
// next telemetry tick.
|
|
1035
|
+
if (!merged.changed && !admission) return false;
|
|
1036
|
+
const remainingS = Math.max(0, Math.round((this.providerCooldown.untilMs - Date.now()) / 1000));
|
|
1037
|
+
this.log(
|
|
1038
|
+
`session runtime provider cooldown from shard ${originShard?.index ?? '-'}`
|
|
1039
|
+
+ ` → ${this.shards.length - 1} sibling shard(s)`
|
|
1040
|
+
+ (admission ? ` [${admission.type}${admission.key ? ` ${admission.key}` : ''}]` : '')
|
|
1041
|
+
+ ` (${this.providerCooldown.disabledReason ? `disabled: ${this.providerCooldown.disabledReason}` : `${remainingS}s`})`,
|
|
1042
|
+
);
|
|
1043
|
+
for (const shard of this.shards) {
|
|
1044
|
+
if (shard === originShard) continue;
|
|
1045
|
+
shard.sendProviderCooldown(this.providerCooldown, admission);
|
|
1046
|
+
}
|
|
1047
|
+
return true;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
refreshWorkload() {
|
|
1051
|
+
if (this.workloadRefresh) return this.workloadRefresh;
|
|
1052
|
+
const live = this.liveShards();
|
|
1053
|
+
if (live.length === 0) {
|
|
1054
|
+
this.workloadCache = { refreshedAt: Date.now(), shards: [] };
|
|
1055
|
+
return Promise.resolve();
|
|
1056
|
+
}
|
|
1057
|
+
this.workloadRefresh = Promise.all(live.map((shard) => shard.workloadSnapshot()))
|
|
1058
|
+
.then((rows) => { this.workloadCache = { refreshedAt: Date.now(), shards: rows }; })
|
|
1059
|
+
.catch(() => { this.workloadCache = { refreshedAt: Date.now(), shards: [] }; })
|
|
1060
|
+
.finally(() => { this.workloadRefresh = null; });
|
|
1061
|
+
return this.workloadRefresh;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async close(reason) {
|
|
1065
|
+
this.closed = true;
|
|
1066
|
+
await Promise.allSettled(this.shards.map((shard) => shard.close(reason)));
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
get status() {
|
|
1070
|
+
const shards = this.shards.map((shard) => shard.status);
|
|
1071
|
+
const live = shards.filter((row) => row.pid);
|
|
1072
|
+
return {
|
|
1073
|
+
shardCount: this.shards.length,
|
|
1074
|
+
shards,
|
|
1075
|
+
// Back-compatible single-worker view for existing status consumers.
|
|
1076
|
+
worker: {
|
|
1077
|
+
pid: live[0]?.pid || null,
|
|
1078
|
+
pids: live.map((row) => row.pid),
|
|
1079
|
+
shards: this.shards.length,
|
|
1080
|
+
runtimes: shards.reduce((total, row) => total + row.runtimes, 0),
|
|
1081
|
+
pending: shards.reduce((total, row) => total + row.pending, 0),
|
|
1082
|
+
recycling: shards.some((row) => row.recycling),
|
|
1083
|
+
degraded: shards.filter((row) => row.degraded).length,
|
|
1084
|
+
},
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
487
1087
|
}
|
|
488
1088
|
|
|
489
1089
|
export function createSessionRuntimeHost({
|
|
@@ -491,12 +1091,33 @@ export function createSessionRuntimeHost({
|
|
|
491
1091
|
cwd = process.cwd(),
|
|
492
1092
|
env = process.env,
|
|
493
1093
|
log = () => {},
|
|
1094
|
+
shardCount = null,
|
|
494
1095
|
} = {}) {
|
|
495
|
-
const
|
|
1096
|
+
const agentSessionListeners = new Set();
|
|
1097
|
+
const agentSessionSnapshots = new Map();
|
|
1098
|
+
const publishAgentSessionState = (update) => {
|
|
1099
|
+
const sessionId = String(update?.sessionId || '');
|
|
1100
|
+
if (!sessionId || !update?.snapshot) return;
|
|
1101
|
+
agentSessionSnapshots.delete(sessionId);
|
|
1102
|
+
agentSessionSnapshots.set(sessionId, update.snapshot);
|
|
1103
|
+
while (agentSessionSnapshots.size > 256) {
|
|
1104
|
+
const oldest = agentSessionSnapshots.keys().next().value;
|
|
1105
|
+
if (oldest === undefined) break;
|
|
1106
|
+
agentSessionSnapshots.delete(oldest);
|
|
1107
|
+
}
|
|
1108
|
+
for (const listener of [...agentSessionListeners]) {
|
|
1109
|
+
try { listener({ sessionId, snapshot: update.snapshot }); } catch {}
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
const pool = new SessionRuntimeShardPool({
|
|
496
1113
|
workerEntry,
|
|
497
1114
|
cwd,
|
|
498
1115
|
env,
|
|
499
1116
|
log,
|
|
1117
|
+
shardCount: Number(shardCount) > 0
|
|
1118
|
+
? normalizeShardCount(shardCount)
|
|
1119
|
+
: resolveShardCount(),
|
|
1120
|
+
onAgentSessionState: publishAgentSessionState,
|
|
500
1121
|
});
|
|
501
1122
|
// One machine-global native counter stays warm in the daemon. Session
|
|
502
1123
|
// runtimes relay requests over the worker IPC channel and never spawn
|
|
@@ -504,40 +1125,20 @@ export function createSessionRuntimeHost({
|
|
|
504
1125
|
try { prewarmNativeTokenCounter(); } catch { /* JS/WASM fallback remains */ }
|
|
505
1126
|
let closed = false;
|
|
506
1127
|
|
|
507
|
-
// Runtime workload telemetry is refreshed lazily with a short TTL
|
|
1128
|
+
// Runtime workload telemetry is refreshed lazily with a short TTL, across
|
|
1129
|
+
// every live shard.
|
|
508
1130
|
const WORKLOAD_TTL_MS = 2_000;
|
|
509
|
-
let workloadCache = { refreshedAt: 0, worker: null };
|
|
510
|
-
let workloadRefresh = null;
|
|
511
1131
|
function refreshRuntimeWorkload() {
|
|
512
|
-
|
|
513
|
-
if (!worker.child || worker.child.killed) {
|
|
514
|
-
workloadCache = { refreshedAt: Date.now(), worker: null };
|
|
515
|
-
return Promise.resolve();
|
|
516
|
-
}
|
|
517
|
-
workloadRefresh = worker.request('workload', {}, 2_000)
|
|
518
|
-
.then((value) => {
|
|
519
|
-
workloadCache = {
|
|
520
|
-
refreshedAt: Date.now(),
|
|
521
|
-
worker: { pid: worker.child?.pid || null, ...value },
|
|
522
|
-
};
|
|
523
|
-
})
|
|
524
|
-
.catch((error) => {
|
|
525
|
-
workloadCache = {
|
|
526
|
-
refreshedAt: Date.now(),
|
|
527
|
-
worker: { pid: worker.child?.pid || null, error: String(error?.message || error) },
|
|
528
|
-
};
|
|
529
|
-
})
|
|
530
|
-
.finally(() => { workloadRefresh = null; });
|
|
531
|
-
return workloadRefresh;
|
|
1132
|
+
return pool.refreshWorkload();
|
|
532
1133
|
}
|
|
533
1134
|
|
|
534
1135
|
return {
|
|
535
1136
|
async create(options = {}) {
|
|
536
1137
|
if (closed) throw new Error('session runtime host is closed');
|
|
537
|
-
return
|
|
1138
|
+
return pool.create(options);
|
|
538
1139
|
},
|
|
539
1140
|
prewarm() {
|
|
540
|
-
return
|
|
1141
|
+
return pool.prewarm();
|
|
541
1142
|
},
|
|
542
1143
|
// Agent execution rides the runtime worker instead of accumulating
|
|
543
1144
|
// provider/orchestrator churn in the daemon.
|
|
@@ -545,17 +1146,23 @@ export function createSessionRuntimeHost({
|
|
|
545
1146
|
if (closed) throw new Error('session runtime host is closed');
|
|
546
1147
|
const dispatchId = String(payload?.dispatchId || '');
|
|
547
1148
|
if (!dispatchId) throw new Error('agent dispatch id is required');
|
|
1149
|
+
// Placement is deterministic and recorded, so cancel/abort always reach
|
|
1150
|
+
// the shard that is actually running this dispatch.
|
|
1151
|
+
const shard = pool.dispatchShard(dispatchId, { create: true });
|
|
548
1152
|
const cancel = (reason) => {
|
|
549
|
-
void
|
|
1153
|
+
void shard.request('agent-dispatch-cancel', {
|
|
550
1154
|
dispatchId,
|
|
551
1155
|
reason: String(reason || 'agent dispatch canceled'),
|
|
552
1156
|
}, 10_000).catch(() => {});
|
|
553
1157
|
};
|
|
554
|
-
if (signal?.aborted)
|
|
1158
|
+
if (signal?.aborted) {
|
|
1159
|
+
pool.releaseDispatch(dispatchId);
|
|
1160
|
+
throw new Error('agent dispatch canceled before start');
|
|
1161
|
+
}
|
|
555
1162
|
const onAbort = () => cancel(signal?.reason?.message || signal?.reason);
|
|
556
1163
|
signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
557
1164
|
try {
|
|
558
|
-
const result = await
|
|
1165
|
+
const result = await shard.request('agent-dispatch', payload, timeoutMs);
|
|
559
1166
|
return result?.value;
|
|
560
1167
|
} catch (error) {
|
|
561
1168
|
// The worker keeps running after a host-side timeout; tell it to stop.
|
|
@@ -563,29 +1170,60 @@ export function createSessionRuntimeHost({
|
|
|
563
1170
|
throw error;
|
|
564
1171
|
} finally {
|
|
565
1172
|
signal?.removeEventListener?.('abort', onAbort);
|
|
1173
|
+
pool.releaseDispatch(dispatchId);
|
|
566
1174
|
}
|
|
567
1175
|
},
|
|
1176
|
+
async agentControl(args = {}, context = {}) {
|
|
1177
|
+
if (closed) throw new Error('session runtime host is closed');
|
|
1178
|
+
const ownerSessionId = String(context?.callerSessionId || '');
|
|
1179
|
+
const ownerIndex = pool.ownership.peek(ownerSessionId);
|
|
1180
|
+
const originShard = pool.shardAt(ownerIndex ?? selectShardIndex(
|
|
1181
|
+
`owner:${ownerSessionId || randomUUID()}`,
|
|
1182
|
+
pool.shardCount,
|
|
1183
|
+
(candidate) => pool.isPlaceable(candidate),
|
|
1184
|
+
));
|
|
1185
|
+
return pool.executeAgentControl(originShard, args, context);
|
|
1186
|
+
},
|
|
568
1187
|
refreshRuntimeWorkload,
|
|
1188
|
+
subscribeAgentSessionStates(listener) {
|
|
1189
|
+
if (typeof listener !== 'function') return () => {};
|
|
1190
|
+
agentSessionListeners.add(listener);
|
|
1191
|
+
for (const [sessionId, snapshot] of agentSessionSnapshots) {
|
|
1192
|
+
try { listener({ sessionId, snapshot }); } catch {}
|
|
1193
|
+
}
|
|
1194
|
+
return () => agentSessionListeners.delete(listener);
|
|
1195
|
+
},
|
|
569
1196
|
get workloads() {
|
|
570
|
-
if (!closed && Date.now() - workloadCache.refreshedAt > WORKLOAD_TTL_MS) {
|
|
1197
|
+
if (!closed && Date.now() - pool.workloadCache.refreshedAt > WORKLOAD_TTL_MS) {
|
|
571
1198
|
void refreshRuntimeWorkload();
|
|
572
1199
|
}
|
|
1200
|
+
const shards = pool.workloadCache.shards;
|
|
573
1201
|
return {
|
|
574
|
-
|
|
1202
|
+
mode: pool.shardCount === 1 ? 'single-runtime' : 'multi-runtime-test',
|
|
1203
|
+
refreshedAt: pool.workloadCache.refreshedAt,
|
|
575
1204
|
machineSpawnBudget: machineSpawnSnapshot(),
|
|
576
|
-
|
|
1205
|
+
shardCount: pool.shardCount,
|
|
1206
|
+
shards,
|
|
1207
|
+
// Aggregate row keeps the historical single-worker shape.
|
|
1208
|
+
worker: aggregateShardWorkload(shards),
|
|
577
1209
|
};
|
|
578
1210
|
},
|
|
579
1211
|
async close(reason = 'session runtime host closed') {
|
|
580
1212
|
if (closed) return;
|
|
581
1213
|
closed = true;
|
|
582
|
-
|
|
1214
|
+
agentSessionListeners.clear();
|
|
1215
|
+
agentSessionSnapshots.clear();
|
|
1216
|
+
await pool.close(reason);
|
|
583
1217
|
},
|
|
584
1218
|
get status() {
|
|
585
|
-
const status =
|
|
1219
|
+
const status = pool.status;
|
|
586
1220
|
return {
|
|
587
|
-
|
|
588
|
-
|
|
1221
|
+
mode: status.shardCount === 1 ? 'single-runtime' : 'multi-runtime-test',
|
|
1222
|
+
active: Boolean(status.worker.pid),
|
|
1223
|
+
worker: status.worker,
|
|
1224
|
+
shards: status.shards,
|
|
1225
|
+
shardCount: status.shardCount,
|
|
1226
|
+
providerCooldown: pool.providerCooldown,
|
|
589
1227
|
};
|
|
590
1228
|
},
|
|
591
1229
|
};
|