mixdog 0.9.53 → 0.9.55
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 +2 -1
- package/scripts/agent-model-liveness-test.mjs +68 -0
- package/scripts/anthropic-transport-policy-test.mjs +260 -0
- package/scripts/desktop-session-bridge-test.mjs +47 -0
- package/scripts/gemini-provider-test.mjs +396 -1
- package/scripts/grok-oauth-refresh-race-test.mjs +76 -1
- package/scripts/interrupted-turn-history-test.mjs +28 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +81 -1
- package/scripts/pending-completion-drop-test.mjs +160 -4
- package/scripts/pending-messages-lock-nonblocking-test.mjs +62 -0
- package/scripts/process-lifecycle-test.mjs +18 -4
- package/scripts/prompt-input-parity-test.mjs +145 -0
- package/scripts/provider-admission-scheduler-test.mjs +4 -5
- package/scripts/provider-contract-test.mjs +91 -33
- package/scripts/provider-toolcall-test.mjs +97 -17
- package/scripts/streaming-tail-window-test.mjs +146 -0
- package/scripts/tui-runtime-load-bench-entry.jsx +608 -0
- package/scripts/tui-runtime-load-bench.mjs +45 -0
- package/scripts/tui-store-frame-batch-test.mjs +99 -0
- package/scripts/tui-transcript-perf-test.mjs +367 -2
- package/scripts/write-backpressure-test.mjs +147 -0
- package/src/cli.mjs +5 -5
- package/src/lib/keychain-cjs.cjs +114 -25
- package/src/repl.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -25
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -2
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +50 -23
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +15 -4
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +136 -27
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +104 -20
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +96 -75
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +40 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +49 -9
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +14 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +9 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +53 -13
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +145 -23
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +20 -4
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +316 -63
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +23 -1
- package/src/runtime/agent/orchestrator/session/store.mjs +46 -1
- package/src/runtime/agent/orchestrator/stall-policy.mjs +6 -13
- package/src/runtime/memory/lib/trace-store.mjs +66 -4
- package/src/runtime/shared/buffered-appender.mjs +83 -4
- package/src/runtime/shared/memory-snapshot.mjs +45 -36
- package/src/runtime/shared/process-lifecycle.mjs +166 -45
- package/src/runtime/shared/process-shutdown.mjs +2 -1
- package/src/session-runtime/model-route-api.mjs +4 -1
- package/src/session-runtime/native-search.mjs +4 -2
- package/src/session-runtime/provider-auth-api.mjs +8 -1
- package/src/session-runtime/provider-models.mjs +8 -3
- package/src/session-runtime/resource-api.mjs +2 -1
- package/src/session-runtime/runtime-core.mjs +32 -0
- package/src/session-runtime/session-turn-api.mjs +1 -0
- package/src/session-runtime/warmup-schedulers.mjs +5 -1
- package/src/standalone/agent-tool.mjs +19 -1
- package/src/tui/App.jsx +10 -4
- package/src/tui/app/text-layout.mjs +9 -1
- package/src/tui/app/transcript-window.mjs +146 -60
- package/src/tui/app/use-mouse-input.mjs +16 -8
- package/src/tui/app/use-transcript-scroll.mjs +71 -19
- package/src/tui/app/use-transcript-window.mjs +21 -10
- package/src/tui/components/PromptInput.jsx +13 -6
- package/src/tui/dist/index.mjs +1094 -232
- package/src/tui/engine/context-state.mjs +31 -31
- package/src/tui/engine/frame-batched-store.mjs +75 -0
- package/src/tui/engine/session-api-ext.mjs +6 -1
- package/src/tui/engine/session-api.mjs +9 -3
- package/src/tui/engine/session-flow.mjs +54 -27
- package/src/tui/engine/turn.mjs +44 -3
- package/src/tui/engine.mjs +515 -36
- package/src/tui/input-editing.mjs +33 -13
- package/src/tui/markdown/measure-rendered-rows.mjs +117 -5
- package/vendor/ink/build/ink.js +8 -16
|
@@ -15,10 +15,12 @@ import {
|
|
|
15
15
|
writeSync,
|
|
16
16
|
} from 'node:fs';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
18
|
+
import { execFile } from 'node:child_process';
|
|
19
|
+
import { isDeepStrictEqual, promisify } from 'node:util';
|
|
20
|
+
import { withFileLock, withFileLockSync } from './atomic-file.mjs';
|
|
20
21
|
import { resolvePluginData } from './plugin-paths.mjs';
|
|
21
22
|
|
|
23
|
+
const execFileAsync = promisify(execFile);
|
|
22
24
|
export const LIFECYCLE_LEDGER_MAX_BYTES = 64 * 1024;
|
|
23
25
|
export const LIFECYCLE_LEDGER_KEEP_FILES = 2;
|
|
24
26
|
const LEDGER_NAME = 'process-lifecycle.jsonl';
|
|
@@ -105,7 +107,35 @@ function appendEntry(active, entry) {
|
|
|
105
107
|
const fd = openSync(active.ledger, 'a', 0o600);
|
|
106
108
|
try {
|
|
107
109
|
writeSync(fd, line, null, 'utf8');
|
|
108
|
-
|
|
110
|
+
} finally {
|
|
111
|
+
closeSync(fd);
|
|
112
|
+
}
|
|
113
|
+
return statSync(active.ledger).size <= LIFECYCLE_LEDGER_MAX_BYTES;
|
|
114
|
+
}, {
|
|
115
|
+
timeoutMs: 0,
|
|
116
|
+
staleMs: LEDGER_LOCK_STALE_MS,
|
|
117
|
+
});
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function appendEntryAsync(active, entry, shouldAppend) {
|
|
124
|
+
const staticLine = typeof entry === 'function' ? null : `${JSON.stringify(entry)}\n`;
|
|
125
|
+
if (staticLine && Buffer.byteLength(staticLine) > LIFECYCLE_LEDGER_MAX_BYTES) return false;
|
|
126
|
+
try {
|
|
127
|
+
return await withFileLock(active.lock, () => {
|
|
128
|
+
if (!shouldAppend()) return false;
|
|
129
|
+
const line = staticLine || `${JSON.stringify(entry())}\n`;
|
|
130
|
+
if (Buffer.byteLength(line) > LIFECYCLE_LEDGER_MAX_BYTES) return false;
|
|
131
|
+
boundPreviousLedger(active);
|
|
132
|
+
if (existsSync(active.ledger)
|
|
133
|
+
&& statSync(active.ledger).size + Buffer.byteLength(line) > LIFECYCLE_LEDGER_MAX_BYTES) {
|
|
134
|
+
rotateLedger(active);
|
|
135
|
+
}
|
|
136
|
+
const fd = openSync(active.ledger, 'a', 0o600);
|
|
137
|
+
try {
|
|
138
|
+
writeSync(fd, line, null, 'utf8');
|
|
109
139
|
} finally {
|
|
110
140
|
closeSync(fd);
|
|
111
141
|
}
|
|
@@ -126,18 +156,30 @@ function currentProcessIdentity() {
|
|
|
126
156
|
? { kind: 'start-seconds', value, method: 'uptime' }
|
|
127
157
|
: null;
|
|
128
158
|
}
|
|
129
|
-
return
|
|
159
|
+
return linuxProcessIdentity(process.pid);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function linuxProcessIdentity(pid) {
|
|
163
|
+
try {
|
|
164
|
+
const raw = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
165
|
+
const fields = raw.slice(raw.lastIndexOf(') ') + 2).trim().split(/\s+/);
|
|
166
|
+
return fields[19] && /^\d+$/.test(fields[19])
|
|
167
|
+
? { kind: 'linux-start-ticks', value: fields[19] }
|
|
168
|
+
: null;
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
130
172
|
}
|
|
131
173
|
|
|
132
|
-
function windowsProcessIdentities(pids) {
|
|
174
|
+
async function windowsProcessIdentities(pids) {
|
|
133
175
|
const identities = new Map();
|
|
134
176
|
if (pids.length === 0) return identities;
|
|
135
177
|
try {
|
|
136
|
-
const
|
|
178
|
+
const { stdout } = await execFileAsync('powershell.exe', [
|
|
137
179
|
'-NoProfile', '-NonInteractive', '-Command',
|
|
138
180
|
`$ErrorActionPreference='SilentlyContinue'; Get-Process -Id @(${pids.join(',')}) | ForEach-Object { try { "$($_.Id):$(([DateTimeOffset]$_.StartTime).ToUnixTimeSeconds())" } catch {} }`,
|
|
139
181
|
], { encoding: 'utf8', timeout: 2000, windowsHide: true });
|
|
140
|
-
for (const line of String(
|
|
182
|
+
for (const line of String(stdout).split(/\r?\n/)) {
|
|
141
183
|
const match = /^(\d+):(\d+)$/.exec(line.trim());
|
|
142
184
|
if (!match) continue;
|
|
143
185
|
const pid = Number(match[1]);
|
|
@@ -150,38 +192,33 @@ function windowsProcessIdentities(pids) {
|
|
|
150
192
|
return identities;
|
|
151
193
|
}
|
|
152
194
|
|
|
153
|
-
function processIdentityForPid(pid) {
|
|
195
|
+
async function processIdentityForPid(pid) {
|
|
154
196
|
if (process.platform === 'linux') {
|
|
155
|
-
|
|
156
|
-
const raw = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
157
|
-
const fields = raw.slice(raw.lastIndexOf(') ') + 2).trim().split(/\s+/);
|
|
158
|
-
return fields[19] && /^\d+$/.test(fields[19])
|
|
159
|
-
? { kind: 'linux-start-ticks', value: fields[19] }
|
|
160
|
-
: null;
|
|
161
|
-
} catch {
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
197
|
+
return linuxProcessIdentity(pid);
|
|
164
198
|
}
|
|
165
199
|
try {
|
|
166
200
|
if (process.platform === 'win32') {
|
|
167
|
-
return windowsProcessIdentities([pid]).get(pid) || null;
|
|
201
|
+
return (await windowsProcessIdentities([pid])).get(pid) || null;
|
|
168
202
|
}
|
|
169
|
-
const
|
|
203
|
+
const { stdout } = await execFileAsync('ps', ['-o', 'lstart=', '-p', String(pid)], {
|
|
170
204
|
encoding: 'utf8',
|
|
171
205
|
timeout: 2000,
|
|
172
206
|
windowsHide: true,
|
|
173
207
|
});
|
|
174
|
-
const value = Math.floor(Date.parse(String(
|
|
208
|
+
const value = Math.floor(Date.parse(String(stdout).trim()) / 1000);
|
|
175
209
|
return Number.isInteger(value) ? { kind: 'start-seconds', value, method: 'ps' } : null;
|
|
176
210
|
} catch {
|
|
177
211
|
return null;
|
|
178
212
|
}
|
|
179
213
|
}
|
|
180
214
|
|
|
181
|
-
function processIdentitiesForPids(pids) {
|
|
215
|
+
async function processIdentitiesForPids(pids) {
|
|
182
216
|
const unique = [...new Set(pids)];
|
|
183
217
|
if (process.platform !== 'win32') {
|
|
184
|
-
return new Map(unique.map((pid) => [
|
|
218
|
+
return new Map(await Promise.all(unique.map(async (pid) => [
|
|
219
|
+
pid,
|
|
220
|
+
await processIdentityForPid(pid),
|
|
221
|
+
])));
|
|
185
222
|
}
|
|
186
223
|
|
|
187
224
|
const identities = new Map();
|
|
@@ -190,7 +227,7 @@ function processIdentitiesForPids(pids) {
|
|
|
190
227
|
if (pid === process.pid) identities.set(pid, currentProcessIdentity());
|
|
191
228
|
else otherPids.push(pid);
|
|
192
229
|
}
|
|
193
|
-
for (const [pid, identity] of windowsProcessIdentities(otherPids)) {
|
|
230
|
+
for (const [pid, identity] of await windowsProcessIdentities(otherPids)) {
|
|
194
231
|
identities.set(pid, identity);
|
|
195
232
|
}
|
|
196
233
|
return identities;
|
|
@@ -217,7 +254,11 @@ function sameProcessIdentity(expected, observed) {
|
|
|
217
254
|
function recordCurrent(reason, exitCode = null) {
|
|
218
255
|
const active = sharedState().active;
|
|
219
256
|
if (!active || !VALID_REASONS.has(reason)) return false;
|
|
220
|
-
return appendEntry(active,
|
|
257
|
+
return appendEntry(active, lifecycleEntry(active, reason, exitCode));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function lifecycleEntry(active, reason, exitCode = null) {
|
|
261
|
+
return {
|
|
221
262
|
version: 1,
|
|
222
263
|
timestamp: new Date().toISOString(),
|
|
223
264
|
pid: process.pid,
|
|
@@ -226,7 +267,7 @@ function recordCurrent(reason, exitCode = null) {
|
|
|
226
267
|
exitCode: Number.isInteger(exitCode) ? exitCode : null,
|
|
227
268
|
memory: memoryCounters(),
|
|
228
269
|
cwd: active.cwd,
|
|
229
|
-
}
|
|
270
|
+
};
|
|
230
271
|
}
|
|
231
272
|
|
|
232
273
|
function recordPriorVanished(active, previous) {
|
|
@@ -240,6 +281,36 @@ function recordPriorVanished(active, previous) {
|
|
|
240
281
|
});
|
|
241
282
|
}
|
|
242
283
|
|
|
284
|
+
function markerMatchesSnapshot(markerPath, previous) {
|
|
285
|
+
try {
|
|
286
|
+
const current = JSON.parse(readFileSync(markerPath, 'utf8'));
|
|
287
|
+
return current?.pid === previous.pid
|
|
288
|
+
&& current?.token === previous.token
|
|
289
|
+
&& isDeepStrictEqual(current?.processIdentity, previous.processIdentity);
|
|
290
|
+
} catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function recordPriorVanishedAsync(active, markerPath, previous) {
|
|
296
|
+
const written = await appendEntryAsync(active, {
|
|
297
|
+
version: 1,
|
|
298
|
+
timestamp: new Date().toISOString(),
|
|
299
|
+
pid: previous.pid,
|
|
300
|
+
ppid: Number.isInteger(previous.ppid) ? previous.ppid : null,
|
|
301
|
+
reason: 'prior-process-vanished',
|
|
302
|
+
exitCode: null,
|
|
303
|
+
}, () => sharedState().active === active && markerMatchesSnapshot(markerPath, previous));
|
|
304
|
+
if (!written || sharedState().active !== active) return false;
|
|
305
|
+
if (!markerMatchesSnapshot(markerPath, previous)) return false;
|
|
306
|
+
try {
|
|
307
|
+
unlinkSync(markerPath);
|
|
308
|
+
return true;
|
|
309
|
+
} catch {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
243
314
|
function pidLiveness(pid) {
|
|
244
315
|
if (!Number.isInteger(pid) || pid < 1 || pid > 2147483647) return 'unknown';
|
|
245
316
|
try {
|
|
@@ -285,17 +356,20 @@ function scheduleProcessIdentityUpgrade(active) {
|
|
|
285
356
|
const scheduleAttempt = (attempt) => {
|
|
286
357
|
if (attempt >= IDENTITY_UPGRADE_DELAYS_MS.length) return;
|
|
287
358
|
const timer = setTimeout(() => {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
359
|
+
void (async () => {
|
|
360
|
+
try {
|
|
361
|
+
if (sharedState().active !== active || !markerOwned(active)) return;
|
|
362
|
+
const processIdentity = await processIdentityForPid(process.pid);
|
|
363
|
+
if (sharedState().active !== active || !markerOwned(active)) return;
|
|
364
|
+
if (processIdentity && processIdentity.method !== 'uptime') {
|
|
365
|
+
const previousIdentity = active.processIdentity;
|
|
366
|
+
active.processIdentity = processIdentity;
|
|
367
|
+
if (writeMarker(active)) return;
|
|
368
|
+
active.processIdentity = previousIdentity;
|
|
369
|
+
}
|
|
370
|
+
} catch {}
|
|
371
|
+
scheduleAttempt(attempt + 1);
|
|
372
|
+
})();
|
|
299
373
|
}, IDENTITY_UPGRADE_DELAYS_MS[attempt]);
|
|
300
374
|
timer.unref?.();
|
|
301
375
|
};
|
|
@@ -325,14 +399,23 @@ function reapVanishedMarkers(active) {
|
|
|
325
399
|
else if (liveness === 'dead' && recordPriorVanished(active, previous)) unlinkSync(markerPath);
|
|
326
400
|
} catch {}
|
|
327
401
|
}
|
|
328
|
-
|
|
402
|
+
void reapOccupiedMarkers(active, occupied).catch(() => {});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function reapOccupiedMarkers(active, occupied) {
|
|
406
|
+
const identities = await processIdentitiesForPids(occupied.map(({ previous }) => previous.pid));
|
|
407
|
+
if (sharedState().active !== active) return;
|
|
329
408
|
for (const { markerPath, previous } of occupied) {
|
|
330
409
|
try {
|
|
410
|
+
if (sharedState().active !== active) return;
|
|
411
|
+
if (!markerMatchesSnapshot(markerPath, previous)) continue;
|
|
331
412
|
const identityMatch = sameProcessIdentity(
|
|
332
413
|
previous.processIdentity,
|
|
333
414
|
identities.get(previous.pid) || null,
|
|
334
415
|
);
|
|
335
|
-
if (identityMatch === false
|
|
416
|
+
if (identityMatch === false) {
|
|
417
|
+
await recordPriorVanishedAsync(active, markerPath, previous);
|
|
418
|
+
}
|
|
336
419
|
} catch {}
|
|
337
420
|
}
|
|
338
421
|
}
|
|
@@ -412,25 +495,63 @@ function strongerReason(current, candidate) {
|
|
|
412
495
|
return (rank[candidate] ?? 0) > (rank[current] ?? -1) ? candidate : current;
|
|
413
496
|
}
|
|
414
497
|
|
|
415
|
-
|
|
416
|
-
const state = sharedState();
|
|
417
|
-
const active = state.active;
|
|
418
|
-
if (!active) return false;
|
|
498
|
+
function accumulateProcessLifecycleFinish(active, reason, exitCode) {
|
|
419
499
|
const finalReason = VALID_REASONS.has(reason) ? reason : 'clean-shutdown';
|
|
420
500
|
const nextReason = strongerReason(active.finalReason, finalReason);
|
|
421
501
|
if (nextReason !== active.finalReason || !Number.isInteger(active.finalExitCode)) {
|
|
422
502
|
active.finalExitCode = exitCode;
|
|
423
503
|
}
|
|
424
504
|
active.finalReason = nextReason;
|
|
425
|
-
|
|
426
|
-
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function claimProcessLifecycleFinish(reason, exitCode) {
|
|
508
|
+
const state = sharedState();
|
|
509
|
+
const active = state.active;
|
|
510
|
+
if (!active) return null;
|
|
511
|
+
accumulateProcessLifecycleFinish(active, reason, exitCode);
|
|
512
|
+
if (active.finishing) return null;
|
|
513
|
+
active.finishing = true;
|
|
514
|
+
return { state, active };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function completeProcessLifecycleFinish(state, active, written) {
|
|
518
|
+
if (!written) {
|
|
519
|
+
active.finishing = false;
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
427
522
|
if (markerOwned(active)) {
|
|
428
|
-
try { unlinkSync(active.markerPath); } catch {
|
|
523
|
+
try { unlinkSync(active.markerPath); } catch {
|
|
524
|
+
active.finishing = false;
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
429
527
|
}
|
|
430
528
|
state.active = null;
|
|
431
529
|
return true;
|
|
432
530
|
}
|
|
433
531
|
|
|
532
|
+
export function finishProcessLifecycle(reason = 'clean-shutdown', exitCode = 0) {
|
|
533
|
+
const claimed = claimProcessLifecycleFinish(reason, exitCode);
|
|
534
|
+
if (!claimed) return false;
|
|
535
|
+
const { state, active } = claimed;
|
|
536
|
+
return completeProcessLifecycleFinish(
|
|
537
|
+
state,
|
|
538
|
+
active,
|
|
539
|
+
appendEntry(active, lifecycleEntry(active, active.finalReason, active.finalExitCode)),
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export async function finishProcessLifecycleAsync(reason = 'clean-shutdown', exitCode = 0) {
|
|
544
|
+
const claimed = claimProcessLifecycleFinish(reason, exitCode);
|
|
545
|
+
if (!claimed) return false;
|
|
546
|
+
const { state, active } = claimed;
|
|
547
|
+
const written = await appendEntryAsync(
|
|
548
|
+
active,
|
|
549
|
+
() => lifecycleEntry(active, active.finalReason, active.finalExitCode),
|
|
550
|
+
() => state.active === active && active.finishing,
|
|
551
|
+
);
|
|
552
|
+
return completeProcessLifecycleFinish(state, active, written);
|
|
553
|
+
}
|
|
554
|
+
|
|
434
555
|
export function lifecyclePathsForTest(directory) {
|
|
435
556
|
return paths(directory);
|
|
436
557
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
finishProcessLifecycle,
|
|
5
|
+
finishProcessLifecycleAsync,
|
|
5
6
|
recordCatchableFatal,
|
|
6
7
|
} from './process-lifecycle.mjs';
|
|
7
8
|
|
|
@@ -125,7 +126,7 @@ export function installProcessSignalCleanup({
|
|
|
125
126
|
}
|
|
126
127
|
running = false;
|
|
127
128
|
if (shouldExit) {
|
|
128
|
-
|
|
129
|
+
await finishProcessLifecycleAsync(
|
|
129
130
|
cleanupFailed ? 'forced-cleanup' : error ? 'catchable-fatal-error' : 'clean-shutdown',
|
|
130
131
|
code,
|
|
131
132
|
);
|
|
@@ -45,7 +45,8 @@ export function createModelRouteApi(deps) {
|
|
|
45
45
|
getConfigHasSecrets, getSearchRouteState, setSearchRouteState,
|
|
46
46
|
cfgMod, reg, mgr, statusRoutes,
|
|
47
47
|
resolveRoute, searchCapableFor, lookupModelMeta,
|
|
48
|
-
adoptConfig, saveConfigAndAdopt, ensureFullConfig,
|
|
48
|
+
adoptConfig, saveConfigAndAdopt, ensureFullConfig, awaitKeychainPrewarm,
|
|
49
|
+
ensureProvidersReady,
|
|
49
50
|
persistLeadRoute, refreshRouteEffort,
|
|
50
51
|
refreshStatuslineUsageSnapshot, scheduleStatuslineUsageRefresh,
|
|
51
52
|
invalidateContextStatusCache, invalidateProviderCaches,
|
|
@@ -70,6 +71,7 @@ export function createModelRouteApi(deps) {
|
|
|
70
71
|
}
|
|
71
72
|
if (!selectedRoute) throw new Error('search route requires provider and model');
|
|
72
73
|
if (isDefaultSearchRouteConfig(selectedRoute)) {
|
|
74
|
+
await awaitKeychainPrewarm();
|
|
73
75
|
ensureFullConfig();
|
|
74
76
|
const routeToSave = normalizeSearchRouteConfig({
|
|
75
77
|
provider: SEARCH_DEFAULT_PROVIDER,
|
|
@@ -87,6 +89,7 @@ export function createModelRouteApi(deps) {
|
|
|
87
89
|
if (!isSearchCapableProvider(selectedRoute.provider)) {
|
|
88
90
|
throw new Error(`provider "${selectedRoute.provider}" does not support Mixdog native search`);
|
|
89
91
|
}
|
|
92
|
+
await awaitKeychainPrewarm();
|
|
90
93
|
ensureFullConfig();
|
|
91
94
|
await ensureProvidersReady(ensureProviderEnabled(getConfig(), selectedRoute.provider));
|
|
92
95
|
const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
|
|
@@ -11,6 +11,7 @@ export function createNativeSearch({
|
|
|
11
11
|
getSession,
|
|
12
12
|
getReg,
|
|
13
13
|
ensureFullConfig,
|
|
14
|
+
awaitKeychainPrewarm,
|
|
14
15
|
ensureProvidersReady,
|
|
15
16
|
ensureProviderEnabled,
|
|
16
17
|
normalizeSearchProviderId,
|
|
@@ -91,8 +92,9 @@ export function createNativeSearch({
|
|
|
91
92
|
return { ...route, id: route.model, display: route.model, name: route.model };
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
function nativeSearchRoutes() {
|
|
95
|
+
async function nativeSearchRoutes() {
|
|
95
96
|
const route = getRoute();
|
|
97
|
+
await awaitKeychainPrewarm();
|
|
96
98
|
const cfg = ensureFullConfig();
|
|
97
99
|
const searchRoute = normalizeSearchRouteConfig(cfg.searchRoute) || normalizeSearchRouteConfig(getSearchRoute());
|
|
98
100
|
setSearchRoute(searchRoute);
|
|
@@ -172,7 +174,7 @@ export function createNativeSearch({
|
|
|
172
174
|
const route = getRoute();
|
|
173
175
|
const session = getSession();
|
|
174
176
|
const reg = getReg();
|
|
175
|
-
const candidates = nativeSearchRoutes();
|
|
177
|
+
const candidates = await nativeSearchRoutes();
|
|
176
178
|
if (!candidates.length) {
|
|
177
179
|
if (isDefaultSearchRouteConfig(getSearchRoute())) {
|
|
178
180
|
throw new Error(`default search route requires the current main model to support native web search (${route?.provider || 'unknown'}/${route?.model || 'unknown'})`);
|
|
@@ -22,6 +22,7 @@ export function createProviderAuthApi({
|
|
|
22
22
|
saveConfigAndAdopt,
|
|
23
23
|
displayConfig,
|
|
24
24
|
reloadFullConfig,
|
|
25
|
+
awaitKeychainPrewarm,
|
|
25
26
|
invalidateProviderCaches,
|
|
26
27
|
warmProviderModelCache,
|
|
27
28
|
refreshProviderCatalogs,
|
|
@@ -52,6 +53,7 @@ export function createProviderAuthApi({
|
|
|
52
53
|
return await getUsageDashboard(options);
|
|
53
54
|
},
|
|
54
55
|
async authenticateProvider(providerId, secret) {
|
|
56
|
+
await awaitKeychainPrewarm();
|
|
55
57
|
const result = String(secret || '').trim()
|
|
56
58
|
? saveProviderApiKey(cfgMod, providerId, secret)
|
|
57
59
|
: await loginOAuthProvider(cfgMod, providerId);
|
|
@@ -62,6 +64,7 @@ export function createProviderAuthApi({
|
|
|
62
64
|
return result;
|
|
63
65
|
},
|
|
64
66
|
async loginOAuthProvider(providerId) {
|
|
67
|
+
await awaitKeychainPrewarm();
|
|
65
68
|
const result = await loginOAuthProvider(cfgMod, providerId);
|
|
66
69
|
reloadFullConfig();
|
|
67
70
|
invalidateProviderCaches();
|
|
@@ -70,11 +73,13 @@ export function createProviderAuthApi({
|
|
|
70
73
|
return result;
|
|
71
74
|
},
|
|
72
75
|
async beginOAuthProviderLogin(providerId) {
|
|
76
|
+
await awaitKeychainPrewarm();
|
|
73
77
|
const result = await beginOAuthProviderLogin(cfgMod, providerId);
|
|
74
78
|
reloadFullConfig();
|
|
75
79
|
return {
|
|
76
80
|
...result,
|
|
77
|
-
waitForCallback: result.waitForCallback?.then((completed) => {
|
|
81
|
+
waitForCallback: result.waitForCallback?.then(async (completed) => {
|
|
82
|
+
await awaitKeychainPrewarm();
|
|
78
83
|
reloadFullConfig();
|
|
79
84
|
if (completed) {
|
|
80
85
|
invalidateProviderCaches();
|
|
@@ -85,6 +90,7 @@ export function createProviderAuthApi({
|
|
|
85
90
|
}),
|
|
86
91
|
completeCode: async (code) => {
|
|
87
92
|
const completed = await result.completeCode(code);
|
|
93
|
+
await awaitKeychainPrewarm();
|
|
88
94
|
reloadFullConfig();
|
|
89
95
|
invalidateProviderCaches();
|
|
90
96
|
refreshProviderCatalogsSoon();
|
|
@@ -114,6 +120,7 @@ export function createProviderAuthApi({
|
|
|
114
120
|
return result;
|
|
115
121
|
},
|
|
116
122
|
async loginOpenCodeGoUsage() {
|
|
123
|
+
await awaitKeychainPrewarm();
|
|
117
124
|
const result = await loginOpenCodeGoUsage(cfgMod);
|
|
118
125
|
reloadFullConfig();
|
|
119
126
|
invalidateProviderCaches();
|
|
@@ -27,6 +27,7 @@ export function createProviderModels({
|
|
|
27
27
|
normalizeSearchProviderId,
|
|
28
28
|
isSearchCapableProvider,
|
|
29
29
|
ensureFullConfig,
|
|
30
|
+
awaitKeychainPrewarm,
|
|
30
31
|
ensureProvidersReady,
|
|
31
32
|
bootProfile,
|
|
32
33
|
scheduleProviderModelWarmup,
|
|
@@ -100,7 +101,8 @@ export function createProviderModels({
|
|
|
100
101
|
return sortProviderModels((rows || []).map(hydrateProviderModelRow));
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
function enabledSearchProviderConfig() {
|
|
104
|
+
async function enabledSearchProviderConfig() {
|
|
105
|
+
await awaitKeychainPrewarm();
|
|
104
106
|
ensureFullConfig();
|
|
105
107
|
const out = {};
|
|
106
108
|
for (const [name, providerConfig] of Object.entries(config().providers || {})) {
|
|
@@ -112,7 +114,7 @@ export function createProviderModels({
|
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
async function loadSearchProviderModelsFresh({ forceRefresh = false } = {}) {
|
|
115
|
-
const searchProviders = enabledSearchProviderConfig();
|
|
117
|
+
const searchProviders = await enabledSearchProviderConfig();
|
|
116
118
|
const providerNames = Object.keys(searchProviders);
|
|
117
119
|
if (!providerNames.length) return [];
|
|
118
120
|
await ensureProvidersReady(config().providers || {});
|
|
@@ -163,7 +165,10 @@ export function createProviderModels({
|
|
|
163
165
|
async function loadProviderModelsFresh({ forceRefresh = false, loadSecrets = true } = {}) {
|
|
164
166
|
const startedAt = performance.now();
|
|
165
167
|
profile('load:start', { forceRefresh, loadSecrets });
|
|
166
|
-
if (loadSecrets)
|
|
168
|
+
if (loadSecrets) {
|
|
169
|
+
await awaitKeychainPrewarm();
|
|
170
|
+
ensureFullConfig();
|
|
171
|
+
}
|
|
167
172
|
const providersStartedAt = performance.now();
|
|
168
173
|
await ensureProvidersReady(config().providers || {});
|
|
169
174
|
profile('providers-ready', { ms: (performance.now() - providersStartedAt).toFixed(1) });
|
|
@@ -32,7 +32,7 @@ export function createResourceApi(deps) {
|
|
|
32
32
|
saveConfigAndAdopt, connectConfiguredMcp, invalidatePreSessionToolSurface,
|
|
33
33
|
recreateCurrentSessionIfReady, normalizeMcpServerInput, mcpStatus,
|
|
34
34
|
skillsStatus, skillContent, addProjectSkill, pluginsStatus, getMemoryModule,
|
|
35
|
-
reloadFullConfig, getActiveTurnCount,
|
|
35
|
+
reloadFullConfig, awaitKeychainPrewarm, getActiveTurnCount,
|
|
36
36
|
} = deps;
|
|
37
37
|
// Per-server MCP toggle serialization. The synchronous config adopt in
|
|
38
38
|
// setMcpServerEnabled has already made the intent durable; the heavy
|
|
@@ -105,6 +105,7 @@ export function createResourceApi(deps) {
|
|
|
105
105
|
return mcpStatus();
|
|
106
106
|
},
|
|
107
107
|
async reconnectMcp() {
|
|
108
|
+
await awaitKeychainPrewarm();
|
|
108
109
|
reloadFullConfig();
|
|
109
110
|
const status = await connectConfiguredMcp({ reset: true });
|
|
110
111
|
invalidatePreSessionToolSurface();
|
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSy
|
|
|
2
2
|
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
3
|
import { performance } from 'node:perf_hooks';
|
|
4
4
|
import httpMod from 'node:http';
|
|
5
|
+
import keychain from '../lib/keychain-cjs.cjs';
|
|
5
6
|
import './hitch-profile.mjs';
|
|
6
7
|
import { ensureStandaloneEnvironment } from '../standalone/seeds.mjs';
|
|
7
8
|
import { createStandaloneAgent } from '../standalone/agent-tool.mjs';
|
|
@@ -285,6 +286,7 @@ export function __applyStandaloneToolDefaultsForTest(tool) {
|
|
|
285
286
|
const resolveDefaultProvider = makeResolveDefaultProvider(isKnownProvider);
|
|
286
287
|
const resolveRoute = makeResolveRoute(resolveDefaultProvider);
|
|
287
288
|
const searchCapableFor = makeSearchCapableFor(normalizeSearchProviderId, isSearchCapableProvider);
|
|
289
|
+
const KEYCHAIN_PREWARM_WAIT_MS = 5000;
|
|
288
290
|
|
|
289
291
|
const outputStyleStatus = (dataDir = STANDALONE_DATA_DIR, opts = {}) => outputStyleStatusRaw(STANDALONE_ROOT, dataDir || STANDALONE_DATA_DIR, opts);
|
|
290
292
|
// Workflow/agent pack loaders bound to this runtime's root/data layout.
|
|
@@ -346,6 +348,26 @@ export async function createMixdogSessionRuntime({
|
|
|
346
348
|
dataDir: STANDALONE_DATA_DIR,
|
|
347
349
|
});
|
|
348
350
|
bootProfile('standalone-env:ready', { ms: (performance.now() - standaloneStartedAt).toFixed(1) });
|
|
351
|
+
const keychainPrewarmPromise = keychain.prewarmSecrets();
|
|
352
|
+
let keychainPrewarmWaitDone = false;
|
|
353
|
+
let keychainPrewarmWaitPromise = null;
|
|
354
|
+
function awaitKeychainPrewarm() {
|
|
355
|
+
if (keychainPrewarmWaitDone) return Promise.resolve();
|
|
356
|
+
keychainPrewarmWaitPromise ??= (async () => {
|
|
357
|
+
let timeoutId;
|
|
358
|
+
const deadline = new Promise((resolveDeadline) => {
|
|
359
|
+
timeoutId = setTimeout(resolveDeadline, KEYCHAIN_PREWARM_WAIT_MS);
|
|
360
|
+
timeoutId.unref?.();
|
|
361
|
+
});
|
|
362
|
+
try {
|
|
363
|
+
await Promise.race([keychainPrewarmPromise, deadline]);
|
|
364
|
+
} finally {
|
|
365
|
+
clearTimeout(timeoutId);
|
|
366
|
+
keychainPrewarmWaitDone = true;
|
|
367
|
+
}
|
|
368
|
+
})();
|
|
369
|
+
return keychainPrewarmWaitPromise;
|
|
370
|
+
}
|
|
349
371
|
|
|
350
372
|
const importsStartedAt = performance.now();
|
|
351
373
|
const [
|
|
@@ -958,6 +980,8 @@ export async function createMixdogSessionRuntime({
|
|
|
958
980
|
mgr,
|
|
959
981
|
dataDir: cfgMod.getPluginData(),
|
|
960
982
|
cwd,
|
|
983
|
+
awaitKeychainPrewarm,
|
|
984
|
+
isKeychainPrewarmReady: () => keychainPrewarmWaitDone,
|
|
961
985
|
// SubagentStart/SubagentStop: bridge internal worker spawn/finish to the
|
|
962
986
|
// standard hook bus. agent_type is passed top-level via hookCommonPayload
|
|
963
987
|
// (added to hook-bus buildEventPayload passthrough). Best-effort.
|
|
@@ -1250,6 +1274,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1250
1274
|
});
|
|
1251
1275
|
|
|
1252
1276
|
async function ensureProvidersReady(providerConfig = config.providers || {}) {
|
|
1277
|
+
await awaitKeychainPrewarm();
|
|
1253
1278
|
const initKey = providerInitCacheKey(providerConfig);
|
|
1254
1279
|
const existing = providerInitPromises.get(initKey);
|
|
1255
1280
|
if (existing) return await existing;
|
|
@@ -1290,6 +1315,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1290
1315
|
getSession: () => session,
|
|
1291
1316
|
getReg: () => reg,
|
|
1292
1317
|
ensureFullConfig,
|
|
1318
|
+
awaitKeychainPrewarm,
|
|
1293
1319
|
ensureProvidersReady,
|
|
1294
1320
|
ensureProviderEnabled,
|
|
1295
1321
|
normalizeSearchProviderId,
|
|
@@ -1352,6 +1378,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1352
1378
|
normalizeSearchProviderId,
|
|
1353
1379
|
isSearchCapableProvider,
|
|
1354
1380
|
ensureFullConfig,
|
|
1381
|
+
awaitKeychainPrewarm,
|
|
1355
1382
|
ensureProvidersReady,
|
|
1356
1383
|
bootProfile,
|
|
1357
1384
|
scheduleProviderModelWarmup: () => scheduleProviderModelWarmupRef(),
|
|
@@ -1460,6 +1487,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1460
1487
|
const startedAt = performance.now();
|
|
1461
1488
|
bootProfile('session:create:start', { mode, reason });
|
|
1462
1489
|
const promise = (async () => {
|
|
1490
|
+
await awaitKeychainPrewarm();
|
|
1463
1491
|
ensureConfigForRouteProvider();
|
|
1464
1492
|
await resolveMissingRouteModelForFirstTurn();
|
|
1465
1493
|
requireModelRoute();
|
|
@@ -1591,6 +1619,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1591
1619
|
getProviderModelsPromise: () => providerModelCaches.providerModelsPromise,
|
|
1592
1620
|
reloadFullConfig,
|
|
1593
1621
|
ensureConfigForRouteProvider,
|
|
1622
|
+
awaitKeychainPrewarm,
|
|
1594
1623
|
ensureProvidersReady,
|
|
1595
1624
|
ensureProviderEnabled,
|
|
1596
1625
|
refreshStatuslineUsageSnapshot,
|
|
@@ -2053,6 +2082,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2053
2082
|
saveConfigAndAdopt,
|
|
2054
2083
|
displayConfig,
|
|
2055
2084
|
reloadFullConfig,
|
|
2085
|
+
awaitKeychainPrewarm,
|
|
2056
2086
|
invalidateProviderCaches,
|
|
2057
2087
|
warmProviderModelCache,
|
|
2058
2088
|
refreshProviderCatalogs: () => ensureProvidersReady(config.providers || {}).then(() => reg.refreshCatalogs()),
|
|
@@ -2120,6 +2150,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2120
2150
|
pluginsStatus,
|
|
2121
2151
|
getMemoryModule,
|
|
2122
2152
|
reloadFullConfig,
|
|
2153
|
+
awaitKeychainPrewarm,
|
|
2123
2154
|
getActiveTurnCount: () => activeTurnCount,
|
|
2124
2155
|
});
|
|
2125
2156
|
const modelRouteApi = createModelRouteApi({
|
|
@@ -2141,6 +2172,7 @@ export async function createMixdogSessionRuntime({
|
|
|
2141
2172
|
adoptConfig,
|
|
2142
2173
|
saveConfigAndAdopt,
|
|
2143
2174
|
ensureFullConfig,
|
|
2175
|
+
awaitKeychainPrewarm,
|
|
2144
2176
|
ensureProvidersReady,
|
|
2145
2177
|
persistLeadRoute,
|
|
2146
2178
|
refreshRouteEffort,
|
|
@@ -167,6 +167,7 @@ export function createSessionTurnApi(deps) {
|
|
|
167
167
|
options.prefetch || null,
|
|
168
168
|
{
|
|
169
169
|
onTextDelta: options.onTextDelta,
|
|
170
|
+
onTextReset: options.onTextReset,
|
|
170
171
|
onReasoningDelta: options.onReasoningDelta,
|
|
171
172
|
onAssistantText: (text) => {
|
|
172
173
|
if (getRemoteEnabled() && getTranscriptWriter()) {
|
|
@@ -18,6 +18,7 @@ export function createWarmupSchedulers({
|
|
|
18
18
|
getProviderModelsPromise,
|
|
19
19
|
reloadFullConfig,
|
|
20
20
|
ensureConfigForRouteProvider,
|
|
21
|
+
awaitKeychainPrewarm,
|
|
21
22
|
ensureProvidersReady,
|
|
22
23
|
ensureProviderEnabled,
|
|
23
24
|
refreshStatuslineUsageSnapshot,
|
|
@@ -50,7 +51,7 @@ export function createWarmupSchedulers({
|
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
53
|
if (timers.providerWarmupTimer || isCloseRequested()) return;
|
|
53
|
-
timers.providerWarmupTimer = setTimeout(() => {
|
|
54
|
+
timers.providerWarmupTimer = setTimeout(async () => {
|
|
54
55
|
timers.providerWarmupTimer = null;
|
|
55
56
|
if (isCloseRequested()) return;
|
|
56
57
|
if (!isFirstTurnCompleted() && !envFlag('MIXDOG_PROVIDER_WARMUP_BEFORE_FIRST_TURN')) {
|
|
@@ -64,6 +65,7 @@ export function createWarmupSchedulers({
|
|
|
64
65
|
}
|
|
65
66
|
const providersStartedAt = performance.now();
|
|
66
67
|
try {
|
|
68
|
+
await awaitKeychainPrewarm();
|
|
67
69
|
reloadFullConfig();
|
|
68
70
|
} catch (error) {
|
|
69
71
|
bootProfile('config:full-failed', { error: error?.message || String(error) });
|
|
@@ -151,6 +153,7 @@ export function createWarmupSchedulers({
|
|
|
151
153
|
return;
|
|
152
154
|
}
|
|
153
155
|
try {
|
|
156
|
+
await awaitKeychainPrewarm();
|
|
154
157
|
ensureConfigForRouteProvider();
|
|
155
158
|
await ensureProvidersReady(ensureProviderEnabled(getConfig(), getRoute().provider));
|
|
156
159
|
if (isCloseRequested()) return;
|
|
@@ -183,6 +186,7 @@ export function createWarmupSchedulers({
|
|
|
183
186
|
return;
|
|
184
187
|
}
|
|
185
188
|
try {
|
|
189
|
+
await awaitKeychainPrewarm();
|
|
186
190
|
ensureConfigForRouteProvider();
|
|
187
191
|
await ensureProvidersReady(ensureProviderEnabled(getConfig(), getRoute().provider));
|
|
188
192
|
if (isCloseRequested()) return;
|