mixdog 0.9.51 → 0.9.52
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/package.json +5 -3
- package/scripts/abort-recovery-test.mjs +17 -1
- package/scripts/agent-model-liveness-test.mjs +79 -2
- package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
- package/scripts/anthropic-transport-policy-test.mjs +466 -0
- package/scripts/atomic-lock-tryonce-test.mjs +60 -1
- package/scripts/build-tui.mjs +13 -1
- package/scripts/channel-daemon-smoke.mjs +630 -10
- package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
- package/scripts/code-graph-disk-hit-test.mjs +11 -0
- package/scripts/code-graph-root-federation-test.mjs +273 -0
- package/scripts/compact-pressure-test.mjs +55 -1
- package/scripts/compact-smoke.mjs +80 -0
- package/scripts/context-mcp-metering-test.mjs +1350 -19
- package/scripts/deferred-tool-loading-test.mjs +17 -0
- package/scripts/gemini-provider-test.mjs +1053 -0
- package/scripts/interrupted-turn-history-test.mjs +371 -0
- package/scripts/lifecycle-api-test.mjs +76 -0
- package/scripts/max-output-recovery-test.mjs +55 -0
- package/scripts/mcp-grace-deferred-test.mjs +89 -13
- package/scripts/memory-pg-recovery-test.mjs +59 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +324 -3
- package/scripts/process-lifecycle-test.mjs +389 -0
- package/scripts/provider-admission-scheduler-test.mjs +582 -0
- package/scripts/provider-contract-test.mjs +268 -0
- package/scripts/provider-toolcall-test.mjs +320 -1
- package/scripts/reactive-compact-persist-smoke.mjs +59 -0
- package/scripts/resource-admission-test.mjs +789 -0
- package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
- package/scripts/steering-drain-buckets-test.mjs +18 -0
- package/scripts/toolcall-args-test.mjs +14 -6
- package/scripts/tui-transcript-perf-test.mjs +5 -7
- package/src/cli.mjs +15 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +26 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +118 -26
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +29 -17
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -38
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +24 -4
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +95 -46
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +89 -17
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +93 -26
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +130 -47
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -17
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +224 -104
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +122 -63
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +69 -37
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +6 -7
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +20 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +89 -22
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +380 -37
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -14
- package/src/runtime/memory/index.mjs +22 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
- package/src/runtime/memory/lib/pg/process.mjs +91 -47
- package/src/runtime/memory/lib/pg/supervisor.mjs +50 -13
- package/src/runtime/shared/atomic-file.mjs +28 -150
- package/src/runtime/shared/process-lifecycle.mjs +363 -0
- package/src/runtime/shared/process-shutdown.mjs +27 -4
- package/src/runtime/shared/resource-admission.mjs +359 -0
- package/src/runtime/shared/staged-child-result.mjs +19 -0
- package/src/session-runtime/context-status.mjs +38 -27
- package/src/session-runtime/lifecycle-api.mjs +26 -6
- package/src/session-runtime/provider-request-tools.mjs +72 -0
- package/src/session-runtime/runtime-core.mjs +6 -3
- package/src/session-runtime/session-turn-api.mjs +5 -4
- package/src/session-runtime/tool-catalog.mjs +375 -15
- package/src/standalone/agent-tool.mjs +17 -38
- package/src/standalone/channel-daemon-client.mjs +200 -38
- package/src/standalone/channel-daemon-transport.mjs +136 -9
- package/src/standalone/channel-worker.mjs +79 -12
- package/src/tui/dist/index.mjs +73 -278
- package/src/tui/engine/session-api-ext.mjs +13 -2
- package/src/tui/engine/session-api.mjs +1 -1
- package/src/tui/engine/turn.mjs +10 -6
- package/src/tui/engine.mjs +13 -4
- package/src/tui/index.jsx +4 -1
- package/src/tui/lib/voice-setup.mjs +16 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { freemem, totalmem } from 'node:os';
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
|
|
4
|
+
const MB = 1024 * 1024;
|
|
5
|
+
|
|
6
|
+
export const RESOURCE_ADMISSION_DEFAULTS = Object.freeze({
|
|
7
|
+
maxAgents: 4,
|
|
8
|
+
maxShells: 4,
|
|
9
|
+
maxHighLoad: 6,
|
|
10
|
+
maxQueue: 32,
|
|
11
|
+
minFreeMemoryMb: 1024,
|
|
12
|
+
maxRssMb: 3072,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function positiveInt(value, fallback) {
|
|
16
|
+
const parsed = Math.floor(Number(value));
|
|
17
|
+
return Number.isFinite(parsed) && parsed >= 1 ? parsed : fallback;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function nonNegativeInt(value, fallback) {
|
|
21
|
+
const parsed = Math.floor(Number(value));
|
|
22
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function envLimits(env = process.env) {
|
|
26
|
+
return {
|
|
27
|
+
maxAgents: positiveInt(env.MIXDOG_MAX_CONCURRENT_AGENTS, RESOURCE_ADMISSION_DEFAULTS.maxAgents),
|
|
28
|
+
maxShells: positiveInt(env.MIXDOG_MAX_CONCURRENT_SHELLS, RESOURCE_ADMISSION_DEFAULTS.maxShells),
|
|
29
|
+
maxHighLoad: positiveInt(env.MIXDOG_MAX_CONCURRENT_HIGH_LOAD, RESOURCE_ADMISSION_DEFAULTS.maxHighLoad),
|
|
30
|
+
maxQueue: nonNegativeInt(env.MIXDOG_RESOURCE_MAX_QUEUE, RESOURCE_ADMISSION_DEFAULTS.maxQueue),
|
|
31
|
+
minFreeMemoryMb: nonNegativeInt(env.MIXDOG_MIN_FREE_MEMORY_MB, RESOURCE_ADMISSION_DEFAULTS.minFreeMemoryMb),
|
|
32
|
+
maxRssMb: nonNegativeInt(env.MIXDOG_MAX_RSS_MB, RESOURCE_ADMISSION_DEFAULTS.maxRssMb),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function defaultMetrics() {
|
|
37
|
+
const usage = process.memoryUsage();
|
|
38
|
+
return {
|
|
39
|
+
rssBytes: usage.rss,
|
|
40
|
+
heapUsedBytes: usage.heapUsed,
|
|
41
|
+
freeMemoryBytes: freemem(),
|
|
42
|
+
totalMemoryBytes: totalmem(),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function abortError(signal) {
|
|
47
|
+
return signal?.reason instanceof Error
|
|
48
|
+
? signal.reason
|
|
49
|
+
: new Error(String(signal?.reason || 'resource admission canceled'));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class ResourcePressureError extends Error {
|
|
53
|
+
constructor(message, details = {}) {
|
|
54
|
+
super(`resource pressure: ${message}`);
|
|
55
|
+
this.name = 'ResourcePressureError';
|
|
56
|
+
this.code = 'ERESOURCEPRESSURE';
|
|
57
|
+
Object.assign(this, details);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class ResourceAdmissionQueueFullError extends ResourcePressureError {
|
|
62
|
+
constructor(maxQueue) {
|
|
63
|
+
super(`high-load admission queue full (maximum ${maxQueue}); retry after running work completes`, {
|
|
64
|
+
maxQueue,
|
|
65
|
+
});
|
|
66
|
+
this.name = 'ResourceAdmissionQueueFullError';
|
|
67
|
+
this.code = 'ERESOURCEQUEUEFULL';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Process-wide admission for memory-heavy agent/explorer work and child shells.
|
|
73
|
+
* Running work is never revoked: pressure only rejects a new request or keeps a
|
|
74
|
+
* concurrency-blocked request in the bounded FIFO queue.
|
|
75
|
+
*/
|
|
76
|
+
export class ResourceAdmissionController {
|
|
77
|
+
constructor({
|
|
78
|
+
limits = {},
|
|
79
|
+
metrics = defaultMetrics,
|
|
80
|
+
now = Date.now,
|
|
81
|
+
env = process.env,
|
|
82
|
+
} = {}) {
|
|
83
|
+
this.limits = { ...envLimits(env), ...limits };
|
|
84
|
+
this.metrics = metrics;
|
|
85
|
+
this.now = now;
|
|
86
|
+
this.active = { agent: 0, shell: 0 };
|
|
87
|
+
this.queue = [];
|
|
88
|
+
this.context = new AsyncLocalStorage();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_memoryError(kind) {
|
|
92
|
+
let sample;
|
|
93
|
+
try { sample = this.metrics() || {}; }
|
|
94
|
+
catch (error) {
|
|
95
|
+
return new ResourcePressureError(`memory metrics unavailable; refusing new ${kind} work`, {
|
|
96
|
+
kind,
|
|
97
|
+
cause: error,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const rssMb = Number(sample.rssBytes) / MB;
|
|
101
|
+
const freeMb = Number(sample.freeMemoryBytes) / MB;
|
|
102
|
+
if (this.limits.maxRssMb > 0 && Number.isFinite(rssMb) && rssMb >= this.limits.maxRssMb) {
|
|
103
|
+
return new ResourcePressureError(
|
|
104
|
+
`Mixdog RSS ${Math.ceil(rssMb)} MB reached ${this.limits.maxRssMb} MB limit; retry after memory recovers`,
|
|
105
|
+
{ kind, rssMb, limitMb: this.limits.maxRssMb, metric: 'rss' },
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (this.limits.minFreeMemoryMb > 0 && Number.isFinite(freeMb) && freeMb < this.limits.minFreeMemoryMb) {
|
|
109
|
+
return new ResourcePressureError(
|
|
110
|
+
`host free memory ${Math.floor(freeMb)} MB is below ${this.limits.minFreeMemoryMb} MB minimum; retry after memory recovers`,
|
|
111
|
+
{ kind, freeMb, limitMb: this.limits.minFreeMemoryMb, metric: 'free-memory' },
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_canStart(kind) {
|
|
118
|
+
const total = this.active.agent + this.active.shell;
|
|
119
|
+
const kindLimit = kind === 'shell' ? this.limits.maxShells : this.limits.maxAgents;
|
|
120
|
+
return total < this.limits.maxHighLoad && this.active[kind] < kindLimit;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_suspendParent(parent) {
|
|
124
|
+
if (!parent || parent.controller !== this || parent.released) return null;
|
|
125
|
+
parent.dependencyDepth += 1;
|
|
126
|
+
if (parent.counted) {
|
|
127
|
+
parent.counted = false;
|
|
128
|
+
this.active[parent.kind] = Math.max(0, this.active[parent.kind] - 1);
|
|
129
|
+
}
|
|
130
|
+
return parent;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
_resumeParent(parent) {
|
|
134
|
+
if (!parent) return Promise.resolve();
|
|
135
|
+
parent.dependencyDepth = Math.max(0, parent.dependencyDepth - 1);
|
|
136
|
+
if (parent.dependencyDepth > 0 || parent.released || parent.counted) return Promise.resolve();
|
|
137
|
+
if (parent.restorePending) return parent.restorePending.promise;
|
|
138
|
+
const pending = Promise.withResolvers();
|
|
139
|
+
const item = {
|
|
140
|
+
restore: true,
|
|
141
|
+
kind: parent.kind,
|
|
142
|
+
label: `restore:${parent.label || parent.kind}`,
|
|
143
|
+
queuedAt: this.now(),
|
|
144
|
+
signal: parent.signal,
|
|
145
|
+
parent,
|
|
146
|
+
resolve: pending.resolve,
|
|
147
|
+
reject: pending.reject,
|
|
148
|
+
canceled: false,
|
|
149
|
+
onAbort: null,
|
|
150
|
+
};
|
|
151
|
+
parent.restorePending = { ...pending, item };
|
|
152
|
+
if (item.signal) {
|
|
153
|
+
item.onAbort = () => {
|
|
154
|
+
const index = this.queue.indexOf(item);
|
|
155
|
+
if (index >= 0) this.queue.splice(index, 1);
|
|
156
|
+
this._detach(item);
|
|
157
|
+
parent.restorePending = null;
|
|
158
|
+
item.reject(abortError(item.signal));
|
|
159
|
+
this._drain();
|
|
160
|
+
};
|
|
161
|
+
item.signal.addEventListener('abort', item.onAbort, { once: true });
|
|
162
|
+
}
|
|
163
|
+
this.queue.push(item);
|
|
164
|
+
this._drain();
|
|
165
|
+
return pending.promise;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
_lease(kind, queuedAt = null, parent = null) {
|
|
169
|
+
this.active[kind] += 1;
|
|
170
|
+
const lease = {
|
|
171
|
+
controller: this,
|
|
172
|
+
kind,
|
|
173
|
+
label: null,
|
|
174
|
+
signal: null,
|
|
175
|
+
counted: true,
|
|
176
|
+
released: false,
|
|
177
|
+
dependencyDepth: 0,
|
|
178
|
+
restorePending: null,
|
|
179
|
+
parent,
|
|
180
|
+
releasePromise: null,
|
|
181
|
+
queuedMs: queuedAt == null ? 0 : Math.max(0, this.now() - queuedAt),
|
|
182
|
+
release: () => {
|
|
183
|
+
if (lease.released) return lease.releasePromise || Promise.resolve();
|
|
184
|
+
lease.released = true;
|
|
185
|
+
if (lease.restorePending) {
|
|
186
|
+
const item = lease.restorePending.item;
|
|
187
|
+
const index = this.queue.indexOf(item);
|
|
188
|
+
if (index >= 0) this.queue.splice(index, 1);
|
|
189
|
+
this._detach(item);
|
|
190
|
+
lease.restorePending = null;
|
|
191
|
+
item.resolve();
|
|
192
|
+
}
|
|
193
|
+
if (lease.counted) {
|
|
194
|
+
lease.counted = false;
|
|
195
|
+
this.active[kind] = Math.max(0, this.active[kind] - 1);
|
|
196
|
+
}
|
|
197
|
+
lease.releasePromise = this._resumeParent(lease.parent);
|
|
198
|
+
this._drain();
|
|
199
|
+
return lease.releasePromise;
|
|
200
|
+
},
|
|
201
|
+
detachDependency: () => {
|
|
202
|
+
if (!lease.parent) return Promise.resolve();
|
|
203
|
+
const dependencyParent = lease.parent;
|
|
204
|
+
lease.parent = null;
|
|
205
|
+
const restored = this._resumeParent(dependencyParent);
|
|
206
|
+
this._drain();
|
|
207
|
+
return restored;
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
return lease;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
runWithLease(lease, task) {
|
|
214
|
+
if (!lease || lease.controller !== this || typeof task !== 'function') {
|
|
215
|
+
return Promise.resolve().then(task);
|
|
216
|
+
}
|
|
217
|
+
return this.context.run(lease, task);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
acquire(kind, { signal = null, label = null, dependency = 'scoped' } = {}) {
|
|
221
|
+
const lane = kind === 'shell' ? 'shell' : 'agent';
|
|
222
|
+
if (signal?.aborted) return Promise.reject(abortError(signal));
|
|
223
|
+
const pressure = this._memoryError(lane);
|
|
224
|
+
if (pressure) return Promise.reject(pressure);
|
|
225
|
+
const ambientParent = this.context.getStore();
|
|
226
|
+
const detachedDependency = dependency === 'detached' && ambientParent?.controller === this;
|
|
227
|
+
const parent = detachedDependency ? null : this._suspendParent(ambientParent);
|
|
228
|
+
if (detachedDependency && (!this._canStart(lane) || this.queue.length > 0)) {
|
|
229
|
+
const error = new ResourcePressureError(
|
|
230
|
+
`detached nested ${lane} work has no admission capacity; retry after running work completes`,
|
|
231
|
+
{ kind: lane },
|
|
232
|
+
);
|
|
233
|
+
error.code = 'ERESOURCEDEPENDENCY';
|
|
234
|
+
return Promise.reject(error);
|
|
235
|
+
}
|
|
236
|
+
if (this._canStart(lane) && this.queue.length === 0) {
|
|
237
|
+
const lease = this._lease(lane, null, parent);
|
|
238
|
+
lease.label = label;
|
|
239
|
+
lease.signal = signal;
|
|
240
|
+
return Promise.resolve(lease);
|
|
241
|
+
}
|
|
242
|
+
if (this.queue.length >= this.limits.maxQueue) {
|
|
243
|
+
const error = new ResourceAdmissionQueueFullError(this.limits.maxQueue);
|
|
244
|
+
return this._resumeParent(parent).then(
|
|
245
|
+
() => Promise.reject(error),
|
|
246
|
+
(restoreError) => Promise.reject(restoreError),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return new Promise((resolve, reject) => {
|
|
250
|
+
const item = {
|
|
251
|
+
kind: lane,
|
|
252
|
+
label,
|
|
253
|
+
queuedAt: this.now(),
|
|
254
|
+
signal,
|
|
255
|
+
resolve,
|
|
256
|
+
reject,
|
|
257
|
+
canceled: false,
|
|
258
|
+
onAbort: null,
|
|
259
|
+
parent,
|
|
260
|
+
};
|
|
261
|
+
if (signal) {
|
|
262
|
+
item.onAbort = () => {
|
|
263
|
+
if (item.canceled) return;
|
|
264
|
+
item.canceled = true;
|
|
265
|
+
const index = this.queue.indexOf(item);
|
|
266
|
+
if (index >= 0) this.queue.splice(index, 1);
|
|
267
|
+
this._detach(item);
|
|
268
|
+
const error = abortError(signal);
|
|
269
|
+
this._resumeParent(item.parent).then(
|
|
270
|
+
() => reject(error),
|
|
271
|
+
(restoreError) => reject(restoreError),
|
|
272
|
+
);
|
|
273
|
+
this._drain();
|
|
274
|
+
};
|
|
275
|
+
signal.addEventListener('abort', item.onAbort, { once: true });
|
|
276
|
+
}
|
|
277
|
+
this.queue.push(item);
|
|
278
|
+
this._drain();
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
_detach(item) {
|
|
283
|
+
if (item.onAbort && item.signal) {
|
|
284
|
+
try { item.signal.removeEventListener('abort', item.onAbort); } catch {}
|
|
285
|
+
item.onAbort = null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
_drain() {
|
|
290
|
+
for (let index = 0; index < this.queue.length;) {
|
|
291
|
+
const item = this.queue[index];
|
|
292
|
+
if (item.restore) {
|
|
293
|
+
if (item.parent.released) {
|
|
294
|
+
this.queue.splice(index, 1);
|
|
295
|
+
this._detach(item);
|
|
296
|
+
item.parent.restorePending = null;
|
|
297
|
+
item.resolve();
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (item.signal?.aborted) {
|
|
301
|
+
this.queue.splice(index, 1);
|
|
302
|
+
this._detach(item);
|
|
303
|
+
item.parent.restorePending = null;
|
|
304
|
+
item.reject(abortError(item.signal));
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (!this._canStart(item.kind)) {
|
|
308
|
+
index += 1;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
this.queue.splice(index, 1);
|
|
312
|
+
this._detach(item);
|
|
313
|
+
item.parent.restorePending = null;
|
|
314
|
+
item.parent.counted = true;
|
|
315
|
+
this.active[item.kind] += 1;
|
|
316
|
+
item.resolve();
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (item.canceled || item.signal?.aborted) {
|
|
320
|
+
this.queue.splice(index, 1);
|
|
321
|
+
this._detach(item);
|
|
322
|
+
const error = abortError(item.signal);
|
|
323
|
+
this._resumeParent(item.parent).then(
|
|
324
|
+
() => item.reject(error),
|
|
325
|
+
(restoreError) => item.reject(restoreError),
|
|
326
|
+
);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (!this._canStart(item.kind)) {
|
|
330
|
+
index += 1;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const pressure = this._memoryError(item.kind);
|
|
334
|
+
this.queue.splice(index, 1);
|
|
335
|
+
this._detach(item);
|
|
336
|
+
if (pressure) {
|
|
337
|
+
this._resumeParent(item.parent).then(
|
|
338
|
+
() => item.reject(pressure),
|
|
339
|
+
(restoreError) => item.reject(restoreError),
|
|
340
|
+
);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const lease = this._lease(item.kind, item.queuedAt, item.parent);
|
|
344
|
+
lease.label = item.label;
|
|
345
|
+
lease.signal = item.signal;
|
|
346
|
+
item.resolve(lease);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
snapshot() {
|
|
351
|
+
return {
|
|
352
|
+
active: { ...this.active },
|
|
353
|
+
queued: this.queue.length,
|
|
354
|
+
limits: { ...this.limits },
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export const resourceAdmission = new ResourceAdmissionController();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { finishProcessLifecycle } from './process-lifecycle.mjs';
|
|
4
|
+
|
|
5
|
+
const SIGNAL_EXIT_CODES = {
|
|
6
|
+
SIGHUP: 129,
|
|
7
|
+
SIGINT: 130,
|
|
8
|
+
SIGTERM: 143,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function stagedChildExitCode(result) {
|
|
12
|
+
if (!result || result.error) return null;
|
|
13
|
+
if (result.signal) {
|
|
14
|
+
const exitCode = SIGNAL_EXIT_CODES[result.signal] || 1;
|
|
15
|
+
finishProcessLifecycle('forced-cleanup', exitCode);
|
|
16
|
+
return exitCode;
|
|
17
|
+
}
|
|
18
|
+
return Number.isInteger(result.status) ? result.status : 1;
|
|
19
|
+
}
|
|
@@ -10,27 +10,29 @@ import {
|
|
|
10
10
|
resolveCompactionPressureTokens,
|
|
11
11
|
resolveWorkerCompactPolicy,
|
|
12
12
|
} from '../runtime/agent/orchestrator/session/loop/compact-policy.mjs';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
import {
|
|
14
|
+
estimateToolSchemaBreakdown,
|
|
15
|
+
snapshotProviderRequestTools,
|
|
16
|
+
} from './tool-catalog.mjs';
|
|
17
|
+
import { scopedProviderRequestTools } from './provider-request-tools.mjs';
|
|
16
18
|
|
|
17
19
|
// Mirrors the tool-list portion of the Anthropic adapters without changing
|
|
18
20
|
// their wire serialization. Other native-deferred providers expose the
|
|
19
21
|
// catalog through BP1/system content, which is already metered there.
|
|
20
|
-
export function requestSerializedToolsForContext(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
export function requestSerializedToolsForContext(
|
|
23
|
+
session,
|
|
24
|
+
provider,
|
|
25
|
+
messages = session?.messages,
|
|
26
|
+
{ nativeTools = [] } = {},
|
|
27
|
+
) {
|
|
28
|
+
return scopedProviderRequestTools(session, provider, messages)?.requestTools
|
|
29
|
+
|| snapshotProviderRequestTools({
|
|
30
|
+
provider,
|
|
31
|
+
tools: session?.tools,
|
|
32
|
+
nativeTools,
|
|
33
|
+
messages,
|
|
34
|
+
session,
|
|
35
|
+
});
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
// Live /context gauge computation + its self-owned memoization cache. Extracted
|
|
@@ -38,13 +40,18 @@ export function requestSerializedToolsForContext(session, provider) {
|
|
|
38
40
|
// the mutable session/route/cwd/mode locals. The cache (key + value) is owned
|
|
39
41
|
// here now, so invalidateContextStatusCache() is returned for the runtime to
|
|
40
42
|
// call from the same places it used to clear the inline locals.
|
|
41
|
-
export function createContextStatus({
|
|
43
|
+
export function createContextStatus({
|
|
44
|
+
getSession,
|
|
45
|
+
getRoute,
|
|
46
|
+
getCurrentCwd,
|
|
47
|
+
getMode,
|
|
48
|
+
getNativeTools = () => [],
|
|
49
|
+
}) {
|
|
42
50
|
let contextStatusCacheKey = null;
|
|
43
51
|
let contextStatusCacheValue = null;
|
|
44
52
|
|
|
45
53
|
function contextStatusCacheKeyFor({
|
|
46
54
|
messages,
|
|
47
|
-
tools,
|
|
48
55
|
messagesRevision,
|
|
49
56
|
toolsSignature,
|
|
50
57
|
requestProvider,
|
|
@@ -68,8 +75,7 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
|
|
|
68
75
|
lastMessage,
|
|
69
76
|
lastMessageRole: lastMessage?.role || null,
|
|
70
77
|
lastMessageContent: lastMessage?.content || null,
|
|
71
|
-
|
|
72
|
-
toolCount: tools.length,
|
|
78
|
+
toolCount: requestToolCount,
|
|
73
79
|
toolsSignature,
|
|
74
80
|
requestProvider,
|
|
75
81
|
requestToolCount,
|
|
@@ -132,17 +138,20 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
|
|
|
132
138
|
// authoritative committed transcript.
|
|
133
139
|
const liveTurnMessages = Array.isArray(session?.liveTurnMessages) ? session.liveTurnMessages : null;
|
|
134
140
|
const messages = liveTurnMessages || (Array.isArray(session?.messages) ? session.messages : []);
|
|
135
|
-
const tools = Array.isArray(session?.tools) ? session.tools : [];
|
|
136
141
|
const requestProvider = session?.provider || route.provider;
|
|
137
|
-
|
|
142
|
+
// Do not even evaluate live native definitions when an in-flight request
|
|
143
|
+
// scope owns the complete immutable provider surface.
|
|
144
|
+
const scopedRequest = scopedProviderRequestTools(session, requestProvider, messages);
|
|
145
|
+
const requestTools = scopedRequest?.requestTools
|
|
146
|
+
|| requestSerializedToolsForContext(session, requestProvider, messages, {
|
|
147
|
+
nativeTools: getNativeTools(),
|
|
148
|
+
});
|
|
138
149
|
const messagesRevision = contextMessagesRevision(messages);
|
|
139
|
-
const toolsSignature = toolSchemaSignature(tools);
|
|
140
150
|
const requestToolsSignature = toolSchemaSignature(requestTools);
|
|
141
151
|
const cacheKey = contextStatusCacheKeyFor({
|
|
142
152
|
messages,
|
|
143
|
-
tools,
|
|
144
153
|
messagesRevision,
|
|
145
|
-
toolsSignature,
|
|
154
|
+
toolsSignature: requestToolsSignature,
|
|
146
155
|
requestProvider,
|
|
147
156
|
requestToolCount: requestTools.length,
|
|
148
157
|
requestToolsSignature,
|
|
@@ -171,7 +180,9 @@ export function createContextStatus({ getSession, getRoute, getCurrentCwd, getMo
|
|
|
171
180
|
// Use the worker policy when a boundary is available so target/reserve
|
|
172
181
|
// headroom, trigger, buffer tokens, and buffer ratio stay identical to the
|
|
173
182
|
// auto-compact decision. Fall back only for incomplete session metadata.
|
|
174
|
-
|
|
183
|
+
// Meter the same pure provider-visible projection used by pre-send
|
|
184
|
+
// compaction and the actual agent-loop send/baseline fingerprint.
|
|
185
|
+
const workerCompactPolicy = resolveWorkerCompactPolicy(session, requestTools);
|
|
175
186
|
const compactPolicy = workerCompactPolicy?.boundaryTokens
|
|
176
187
|
? workerCompactPolicy
|
|
177
188
|
: resolveSessionCompactPolicy(session || {}, compactBoundaryTokens);
|
|
@@ -12,6 +12,20 @@ import {
|
|
|
12
12
|
import { toolSpecForMode, deferredSurfaceModeForLead } from './effort.mjs';
|
|
13
13
|
import { unregisterLiveSession } from '../runtime/shared/staged-update.mjs';
|
|
14
14
|
|
|
15
|
+
export function resolveResumeCwd(session, currentCwd) {
|
|
16
|
+
const desktop = session?.desktopSession;
|
|
17
|
+
if (desktop?.classification === 'project') {
|
|
18
|
+
return clean(desktop.projectPath) || session?.cwd || currentCwd;
|
|
19
|
+
}
|
|
20
|
+
if (desktop?.classification === 'task') {
|
|
21
|
+
// Desktop task sessions deliberately stay in the app-managed unclassified
|
|
22
|
+
// workspace selected by the host, even if an old transcript recorded a
|
|
23
|
+
// transient cwd. CLI/TUI sessions have no metadata and retain old behavior.
|
|
24
|
+
return currentCwd;
|
|
25
|
+
}
|
|
26
|
+
return session?.cwd || currentCwd;
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
// Session lifecycle surface: teardown (close/abort), resume/new, and the
|
|
16
30
|
// resumable-session listing. Extracted verbatim from the runtime API object;
|
|
17
31
|
// stateless helpers are imported directly and the runtime injects live
|
|
@@ -30,7 +44,7 @@ export function createLifecycleApi(deps) {
|
|
|
30
44
|
invalidateContextStatusCache, invalidatePreSessionToolSurface,
|
|
31
45
|
applyResolvedCwd, resolveRoute, applyDeferredToolSurface, standaloneTools,
|
|
32
46
|
pushTranscriptRebind,
|
|
33
|
-
notificationListeners, remoteStateListeners,
|
|
47
|
+
notificationListeners, remoteStateListeners, desktopSession,
|
|
34
48
|
} = deps;
|
|
35
49
|
return {
|
|
36
50
|
async close(reason = 'cli-exit', options = {}) {
|
|
@@ -99,7 +113,7 @@ export function createLifecycleApi(deps) {
|
|
|
99
113
|
try { agentTool.closeAll(reason); } catch {}
|
|
100
114
|
let mcpStop = null;
|
|
101
115
|
try { mcpStop = mcpClient.disconnectAll?.(); } catch {}
|
|
102
|
-
const openaiWsStop = globalThis.__mixdogOpenaiWsRuntimeLoaded === true
|
|
116
|
+
const openaiWsStop = isProcessExit && globalThis.__mixdogOpenaiWsRuntimeLoaded === true
|
|
103
117
|
? import('../runtime/agent/orchestrator/providers/openai-oauth-ws.mjs')
|
|
104
118
|
.then((mod) => mod?.drainOpenaiWsPool?.(reason))
|
|
105
119
|
.catch(() => {})
|
|
@@ -174,8 +188,10 @@ export function createLifecycleApi(deps) {
|
|
|
174
188
|
if (!session?.id) return false;
|
|
175
189
|
return mgr.abortSessionTurn(session.id, reason);
|
|
176
190
|
},
|
|
177
|
-
listSessions() {
|
|
178
|
-
return mgr.listSessions({
|
|
191
|
+
listSessions(options = {}) {
|
|
192
|
+
return mgr.listSessions({
|
|
193
|
+
refreshFromStorage: options?.refreshFromStorage === true,
|
|
194
|
+
}).map(s => {
|
|
179
195
|
const owner = clean(s.owner || 'user').toLowerCase();
|
|
180
196
|
if (owner && !['cli', 'user', 'mixdog', 'legacy'].includes(owner)) return null;
|
|
181
197
|
const sourceType = clean(s.sourceType || '').toLowerCase();
|
|
@@ -206,6 +222,7 @@ export function createLifecycleApi(deps) {
|
|
|
206
222
|
provider: s.provider,
|
|
207
223
|
messageCount,
|
|
208
224
|
preview,
|
|
225
|
+
desktopSession: s.desktopSession || null,
|
|
209
226
|
};
|
|
210
227
|
}).filter(Boolean);
|
|
211
228
|
},
|
|
@@ -230,7 +247,10 @@ export function createLifecycleApi(deps) {
|
|
|
230
247
|
const previousId = prev?.id || null;
|
|
231
248
|
const previousMessages = prev?.messages || null;
|
|
232
249
|
const previousLive = prev?.liveTurnMessages || null;
|
|
233
|
-
const
|
|
250
|
+
const resumeOptions = desktopSession && typeof desktopSession === 'object'
|
|
251
|
+
? { desktopSession }
|
|
252
|
+
: undefined;
|
|
253
|
+
const resumed = await mgr.resumeSession(id, toolSpecForMode(getMode()), resumeOptions);
|
|
234
254
|
if (!resumed) return null;
|
|
235
255
|
if (previousId && previousId !== resumed.id) {
|
|
236
256
|
statusRoutes?.clearGatewaySessionRoute?.(previousId);
|
|
@@ -239,7 +259,7 @@ export function createLifecycleApi(deps) {
|
|
|
239
259
|
mgr.closeSession(previousId, 'cli-resume', { tombstone });
|
|
240
260
|
}
|
|
241
261
|
setSession(resumed);
|
|
242
|
-
applyResolvedCwd(resumed
|
|
262
|
+
applyResolvedCwd(resolveResumeCwd(resumed, getCurrentCwd()), { markRefresh: false });
|
|
243
263
|
const route = getRoute();
|
|
244
264
|
const resumeEffort = hasOwn(route, 'effort') ? route.effort : resumed.effort;
|
|
245
265
|
setRoute(resolveRoute(getConfig(), { provider: resumed.provider, model: resumed.model, effort: resumeEffort }));
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
|
|
3
|
+
const requestToolScope = new AsyncLocalStorage();
|
|
4
|
+
const NATIVE_PREFIX_COUNT = Symbol('mixdog.providerNativeToolPrefixCount');
|
|
5
|
+
let requestToolScopeGeneration = 0;
|
|
6
|
+
|
|
7
|
+
function normalizedProvider(provider) {
|
|
8
|
+
return String(provider || '').trim().toLowerCase();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function providerNativeToolPrefixCount(requestTools, fallback = 0) {
|
|
12
|
+
const raw = Array.isArray(requestTools) && Number.isInteger(requestTools[NATIVE_PREFIX_COUNT])
|
|
13
|
+
? requestTools[NATIVE_PREFIX_COUNT]
|
|
14
|
+
: fallback;
|
|
15
|
+
return Math.max(0, Math.min(Array.isArray(requestTools) ? requestTools.length : 0, Number(raw) || 0));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function finalizeProviderRequestTools(requestTools, nativePrefixCount = 0) {
|
|
19
|
+
Object.defineProperty(requestTools, NATIVE_PREFIX_COUNT, {
|
|
20
|
+
value: Math.max(0, Math.min(requestTools.length, Number(nativePrefixCount) || 0)),
|
|
21
|
+
enumerable: false,
|
|
22
|
+
configurable: false,
|
|
23
|
+
writable: false,
|
|
24
|
+
});
|
|
25
|
+
return Object.freeze(requestTools);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function runWithProviderRequestToolsScope(scope, callback) {
|
|
29
|
+
const store = {
|
|
30
|
+
session: scope.session,
|
|
31
|
+
provider: normalizedProvider(scope.provider),
|
|
32
|
+
messages: scope.messages,
|
|
33
|
+
requestTools: scope.requestTools,
|
|
34
|
+
nativePrefixCount: providerNativeToolPrefixCount(
|
|
35
|
+
scope.requestTools,
|
|
36
|
+
scope.nativePrefixCount,
|
|
37
|
+
),
|
|
38
|
+
generation: ++requestToolScopeGeneration,
|
|
39
|
+
active: true,
|
|
40
|
+
};
|
|
41
|
+
return requestToolScope.run(store, () => {
|
|
42
|
+
let result;
|
|
43
|
+
try {
|
|
44
|
+
result = callback();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
store.active = false;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
if (result && typeof result.then === 'function') {
|
|
50
|
+
return Promise.resolve(result).finally(() => {
|
|
51
|
+
store.active = false;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
store.active = false;
|
|
55
|
+
return result;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function invalidateProviderRequestToolsScope(scope = requestToolScope.getStore()) {
|
|
60
|
+
if (scope && typeof scope === 'object') scope.active = false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function scopedProviderRequestTools(session, provider, messages) {
|
|
64
|
+
const scope = requestToolScope.getStore();
|
|
65
|
+
return scope
|
|
66
|
+
&& scope.active === true
|
|
67
|
+
&& scope.session === session
|
|
68
|
+
&& scope.provider === normalizedProvider(provider)
|
|
69
|
+
&& scope.messages === messages
|
|
70
|
+
? scope
|
|
71
|
+
: null;
|
|
72
|
+
}
|
|
@@ -363,6 +363,7 @@ export async function createMixdogSessionRuntime({
|
|
|
363
363
|
cwd = process.cwd(),
|
|
364
364
|
toolMode = 'full',
|
|
365
365
|
remote = false,
|
|
366
|
+
desktopSession = null,
|
|
366
367
|
} = {}) {
|
|
367
368
|
bootProfile('session-runtime:start', { provider, model, toolMode, cwd });
|
|
368
369
|
let remoteEnabled = remote === true;
|
|
@@ -1555,6 +1556,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1555
1556
|
clientHostPid: process.pid,
|
|
1556
1557
|
disallowedTools: LEAD_DISALLOWED_TOOLS,
|
|
1557
1558
|
cwd: currentCwd,
|
|
1559
|
+
...(desktopSession && typeof desktopSession === 'object' ? { desktopSession } : {}),
|
|
1558
1560
|
coreMemoryContext,
|
|
1559
1561
|
workflow,
|
|
1560
1562
|
workflowContext,
|
|
@@ -1569,11 +1571,11 @@ export async function createMixdogSessionRuntime({
|
|
|
1569
1571
|
session = mgr.createSession(sessionOpts);
|
|
1570
1572
|
sessionNeedsCwdRefresh = false;
|
|
1571
1573
|
attachSessionHooks(session, { hooks, hookCommonPayload, getCwd: () => currentCwd });
|
|
1572
|
-
// Every-create MCP fold (NO blocking): seed the INITIAL
|
|
1573
|
-
// BP1 manifest from
|
|
1574
|
+
// Every-create MCP fold (NO blocking): seed the INITIAL provider-visible
|
|
1575
|
+
// surface (and native BP1 manifest) from MCP servers connected at create
|
|
1574
1576
|
// time. There is no await — a boot connect still mid-handshake is caught on
|
|
1575
1577
|
// the first user turn by refreshInitialDeferredMcpSurface (session-turn-api),
|
|
1576
|
-
// which re-folds the live registry into the
|
|
1578
|
+
// which re-folds the live registry into the first-turn surface before the
|
|
1577
1579
|
// prompt renders. This fold keeps recreate paths (cwd change with MCP
|
|
1578
1580
|
// already connected) seeding their manifest instead of re-announcing late.
|
|
1579
1581
|
let connectedMcpTools = [];
|
|
@@ -2155,6 +2157,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2155
2157
|
resolveRoute,
|
|
2156
2158
|
applyDeferredToolSurface,
|
|
2157
2159
|
standaloneTools,
|
|
2160
|
+
desktopSession,
|
|
2158
2161
|
});
|
|
2159
2162
|
const resourceApi = createResourceApi({
|
|
2160
2163
|
getConfig: () => config,
|