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.
Files changed (110) hide show
  1. package/package.json +5 -3
  2. package/scripts/abort-recovery-test.mjs +17 -1
  3. package/scripts/agent-model-liveness-test.mjs +79 -2
  4. package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
  5. package/scripts/anthropic-transport-policy-test.mjs +466 -0
  6. package/scripts/atomic-lock-tryonce-test.mjs +60 -1
  7. package/scripts/build-tui.mjs +13 -1
  8. package/scripts/channel-daemon-smoke.mjs +630 -10
  9. package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
  10. package/scripts/code-graph-disk-hit-test.mjs +11 -0
  11. package/scripts/code-graph-root-federation-test.mjs +273 -0
  12. package/scripts/compact-pressure-test.mjs +55 -1
  13. package/scripts/compact-smoke.mjs +80 -0
  14. package/scripts/context-mcp-metering-test.mjs +1350 -19
  15. package/scripts/deferred-tool-loading-test.mjs +17 -0
  16. package/scripts/gemini-provider-test.mjs +1053 -0
  17. package/scripts/interrupted-turn-history-test.mjs +371 -0
  18. package/scripts/lifecycle-api-test.mjs +76 -0
  19. package/scripts/max-output-recovery-test.mjs +55 -0
  20. package/scripts/mcp-grace-deferred-test.mjs +89 -13
  21. package/scripts/memory-pg-recovery-test.mjs +59 -0
  22. package/scripts/openai-oauth-ws-1006-retry-test.mjs +324 -3
  23. package/scripts/process-lifecycle-test.mjs +389 -0
  24. package/scripts/provider-admission-scheduler-test.mjs +582 -0
  25. package/scripts/provider-contract-test.mjs +268 -0
  26. package/scripts/provider-toolcall-test.mjs +320 -1
  27. package/scripts/reactive-compact-persist-smoke.mjs +59 -0
  28. package/scripts/resource-admission-test.mjs +789 -0
  29. package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
  30. package/scripts/steering-drain-buckets-test.mjs +18 -0
  31. package/scripts/toolcall-args-test.mjs +14 -6
  32. package/scripts/tui-transcript-perf-test.mjs +5 -7
  33. package/src/cli.mjs +15 -2
  34. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +26 -0
  35. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
  36. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +118 -26
  38. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +29 -17
  39. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -38
  40. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +24 -4
  41. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
  42. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
  43. package/src/runtime/agent/orchestrator/providers/gemini.mjs +95 -46
  44. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -4
  45. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
  46. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
  47. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -3
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
  49. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +89 -17
  50. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +93 -26
  51. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +130 -47
  52. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -8
  53. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -3
  54. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -17
  55. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
  56. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
  57. package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
  58. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +224 -104
  59. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +122 -63
  60. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
  61. package/src/runtime/agent/orchestrator/session/context-utils.mjs +17 -1
  62. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +69 -37
  63. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
  64. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
  65. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +6 -7
  66. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
  67. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
  68. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
  69. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
  70. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +20 -4
  71. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
  72. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
  73. package/src/runtime/agent/orchestrator/session/store.mjs +89 -22
  74. package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
  75. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
  76. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
  77. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
  78. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +380 -37
  79. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
  80. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
  81. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
  82. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
  83. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
  84. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -14
  85. package/src/runtime/memory/index.mjs +22 -4
  86. package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
  87. package/src/runtime/memory/lib/pg/process.mjs +91 -47
  88. package/src/runtime/memory/lib/pg/supervisor.mjs +50 -13
  89. package/src/runtime/shared/atomic-file.mjs +28 -150
  90. package/src/runtime/shared/process-lifecycle.mjs +363 -0
  91. package/src/runtime/shared/process-shutdown.mjs +27 -4
  92. package/src/runtime/shared/resource-admission.mjs +359 -0
  93. package/src/runtime/shared/staged-child-result.mjs +19 -0
  94. package/src/session-runtime/context-status.mjs +38 -27
  95. package/src/session-runtime/lifecycle-api.mjs +26 -6
  96. package/src/session-runtime/provider-request-tools.mjs +72 -0
  97. package/src/session-runtime/runtime-core.mjs +6 -3
  98. package/src/session-runtime/session-turn-api.mjs +5 -4
  99. package/src/session-runtime/tool-catalog.mjs +375 -15
  100. package/src/standalone/agent-tool.mjs +17 -38
  101. package/src/standalone/channel-daemon-client.mjs +200 -38
  102. package/src/standalone/channel-daemon-transport.mjs +136 -9
  103. package/src/standalone/channel-worker.mjs +79 -12
  104. package/src/tui/dist/index.mjs +73 -278
  105. package/src/tui/engine/session-api-ext.mjs +13 -2
  106. package/src/tui/engine/session-api.mjs +1 -1
  107. package/src/tui/engine/turn.mjs +10 -6
  108. package/src/tui/engine.mjs +13 -4
  109. package/src/tui/index.jsx +4 -1
  110. 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 { estimateToolSchemaBreakdown } from './tool-catalog.mjs';
14
-
15
- const DEFERRED_CATALOG_TOOL_PROVIDERS = new Set(['anthropic', 'anthropic-oauth']);
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(session, provider) {
21
- const activeTools = Array.isArray(session?.tools) ? session.tools : [];
22
- const normalizedProvider = String(provider || '').trim().toLowerCase();
23
- if (!DEFERRED_CATALOG_TOOL_PROVIDERS.has(normalizedProvider)
24
- || session?.deferredNativeTools !== true
25
- || activeTools.length === 0) {
26
- return activeTools;
27
- }
28
- const activeNames = new Set(activeTools.map((tool) => String(tool?.name || '').trim()).filter(Boolean));
29
- const catalog = Array.isArray(session?.deferredToolCatalog) ? session.deferredToolCatalog : [];
30
- const deferredTools = catalog
31
- .filter((tool) => tool?.name && !activeNames.has(String(tool.name)))
32
- .map((tool) => ({ ...tool, deferLoading: true }));
33
- return deferredTools.length ? [...activeTools, ...deferredTools] : activeTools;
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({ getSession, getRoute, getCurrentCwd, getMode }) {
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
- tools,
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
- const requestTools = requestSerializedToolsForContext(session, requestProvider);
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
- const workerCompactPolicy = resolveWorkerCompactPolicy(session, tools);
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({}).map(s => {
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 resumed = await mgr.resumeSession(id, toolSpecForMode(getMode()));
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.cwd || getCurrentCwd(), { markRefresh: false });
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 deferred surface +
1573
- // BP1 manifest from whatever MCP servers are ALREADY connected at create
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 initial manifest before 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,