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.
Files changed (77) hide show
  1. package/package.json +2 -1
  2. package/scripts/agent-model-liveness-test.mjs +68 -0
  3. package/scripts/anthropic-transport-policy-test.mjs +260 -0
  4. package/scripts/desktop-session-bridge-test.mjs +47 -0
  5. package/scripts/gemini-provider-test.mjs +396 -1
  6. package/scripts/grok-oauth-refresh-race-test.mjs +76 -1
  7. package/scripts/interrupted-turn-history-test.mjs +28 -0
  8. package/scripts/openai-oauth-ws-1006-retry-test.mjs +81 -1
  9. package/scripts/pending-completion-drop-test.mjs +160 -4
  10. package/scripts/pending-messages-lock-nonblocking-test.mjs +62 -0
  11. package/scripts/process-lifecycle-test.mjs +18 -4
  12. package/scripts/prompt-input-parity-test.mjs +145 -0
  13. package/scripts/provider-admission-scheduler-test.mjs +4 -5
  14. package/scripts/provider-contract-test.mjs +91 -33
  15. package/scripts/provider-toolcall-test.mjs +97 -17
  16. package/scripts/streaming-tail-window-test.mjs +146 -0
  17. package/scripts/tui-runtime-load-bench-entry.jsx +608 -0
  18. package/scripts/tui-runtime-load-bench.mjs +45 -0
  19. package/scripts/tui-store-frame-batch-test.mjs +99 -0
  20. package/scripts/tui-transcript-perf-test.mjs +367 -2
  21. package/scripts/write-backpressure-test.mjs +147 -0
  22. package/src/cli.mjs +5 -5
  23. package/src/lib/keychain-cjs.cjs +114 -25
  24. package/src/repl.mjs +11 -0
  25. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -25
  26. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -2
  27. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +50 -23
  28. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +15 -4
  29. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +136 -27
  30. package/src/runtime/agent/orchestrator/providers/gemini.mjs +104 -20
  31. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +96 -75
  32. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +40 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +5 -0
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +35 -4
  35. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +3 -1
  36. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +49 -9
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +14 -2
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +9 -4
  39. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +53 -13
  40. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +145 -23
  41. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +20 -4
  42. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +316 -63
  43. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +23 -1
  44. package/src/runtime/agent/orchestrator/session/store.mjs +46 -1
  45. package/src/runtime/agent/orchestrator/stall-policy.mjs +6 -13
  46. package/src/runtime/memory/lib/trace-store.mjs +66 -4
  47. package/src/runtime/shared/buffered-appender.mjs +83 -4
  48. package/src/runtime/shared/memory-snapshot.mjs +45 -36
  49. package/src/runtime/shared/process-lifecycle.mjs +166 -45
  50. package/src/runtime/shared/process-shutdown.mjs +2 -1
  51. package/src/session-runtime/model-route-api.mjs +4 -1
  52. package/src/session-runtime/native-search.mjs +4 -2
  53. package/src/session-runtime/provider-auth-api.mjs +8 -1
  54. package/src/session-runtime/provider-models.mjs +8 -3
  55. package/src/session-runtime/resource-api.mjs +2 -1
  56. package/src/session-runtime/runtime-core.mjs +32 -0
  57. package/src/session-runtime/session-turn-api.mjs +1 -0
  58. package/src/session-runtime/warmup-schedulers.mjs +5 -1
  59. package/src/standalone/agent-tool.mjs +19 -1
  60. package/src/tui/App.jsx +10 -4
  61. package/src/tui/app/text-layout.mjs +9 -1
  62. package/src/tui/app/transcript-window.mjs +146 -60
  63. package/src/tui/app/use-mouse-input.mjs +16 -8
  64. package/src/tui/app/use-transcript-scroll.mjs +71 -19
  65. package/src/tui/app/use-transcript-window.mjs +21 -10
  66. package/src/tui/components/PromptInput.jsx +13 -6
  67. package/src/tui/dist/index.mjs +1094 -232
  68. package/src/tui/engine/context-state.mjs +31 -31
  69. package/src/tui/engine/frame-batched-store.mjs +75 -0
  70. package/src/tui/engine/session-api-ext.mjs +6 -1
  71. package/src/tui/engine/session-api.mjs +9 -3
  72. package/src/tui/engine/session-flow.mjs +54 -27
  73. package/src/tui/engine/turn.mjs +44 -3
  74. package/src/tui/engine.mjs +515 -36
  75. package/src/tui/input-editing.mjs +33 -13
  76. package/src/tui/markdown/measure-rendered-rows.mjs +117 -5
  77. 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 { execFileSync } from 'node:child_process';
19
- import { withFileLockSync } from './atomic-file.mjs';
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
- fsyncSync(fd);
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 processIdentityForPid(process.pid);
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 out = execFileSync('powershell.exe', [
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(out).split(/\r?\n/)) {
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
- try {
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 out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
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(out).trim()) / 1000);
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) => [pid, processIdentityForPid(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
- try {
289
- if (sharedState().active !== active || !markerOwned(active)) return;
290
- const processIdentity = processIdentityForPid(process.pid);
291
- if (processIdentity && processIdentity.method !== 'uptime') {
292
- const previousIdentity = active.processIdentity;
293
- active.processIdentity = processIdentity;
294
- if (writeMarker(active)) return;
295
- active.processIdentity = previousIdentity;
296
- }
297
- } catch {}
298
- scheduleAttempt(attempt + 1);
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
- const identities = processIdentitiesForPids(occupied.map(({ previous }) => previous.pid));
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 && recordPriorVanished(active, previous)) unlinkSync(markerPath);
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
- export function finishProcessLifecycle(reason = 'clean-shutdown', exitCode = 0) {
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
- const written = recordCurrent(active.finalReason, active.finalExitCode);
426
- if (!written) return false;
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 { return false; }
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
- finishProcessLifecycle(
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, ensureProvidersReady,
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) ensureFullConfig();
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;