mixdog 0.9.16 → 0.9.17

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 (44) hide show
  1. package/package.json +2 -1
  2. package/scripts/atomic-lock-tryonce-test.mjs +66 -0
  3. package/scripts/bench/cache-probe-tasks.json +1 -1
  4. package/scripts/bench/lead-review-tasks-r3.json +1 -1
  5. package/scripts/bench/r4-mixed-tasks.json +1 -1
  6. package/scripts/bench/review-tasks.json +1 -1
  7. package/scripts/build-runtime-windows.ps1 +242 -242
  8. package/scripts/provider-toolcall-test.mjs +79 -2
  9. package/scripts/recall-usecase-cases.json +1 -1
  10. package/scripts/smoke-runtime-negative.ps1 +106 -106
  11. package/scripts/tool-efficiency-diag.mjs +1 -1
  12. package/src/mixdog-session-runtime.mjs +12 -0
  13. package/src/rules/lead/02-channels.md +3 -3
  14. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +33 -1
  15. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +9 -0
  16. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +49 -0
  17. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -0
  18. package/src/runtime/agent/orchestrator/session/context-utils.mjs +7 -0
  19. package/src/runtime/agent/orchestrator/session/loop.mjs +8 -0
  20. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +32 -18
  21. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +142 -3
  22. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +108 -30
  23. package/src/runtime/agent/orchestrator/session/store.mjs +5 -0
  24. package/src/runtime/agent/orchestrator/stall-policy.mjs +22 -12
  25. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +3 -1
  26. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +15 -15
  27. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +10 -2
  28. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +8 -2
  29. package/src/runtime/channels/lib/runtime-paths.mjs +8 -19
  30. package/src/runtime/memory/index.mjs +37 -0
  31. package/src/runtime/memory/lib/embedding-warmup.mjs +3 -0
  32. package/src/runtime/memory/lib/ko-morph.mjs +1 -0
  33. package/src/runtime/shared/atomic-file.mjs +110 -0
  34. package/src/runtime/shared/transcript-writer.mjs +46 -4
  35. package/src/session-runtime/provider-models.mjs +47 -8
  36. package/src/tui/app/transcript-window.mjs +115 -6
  37. package/src/tui/app/use-transcript-window.mjs +58 -7
  38. package/src/tui/components/StatusLine.jsx +1 -1
  39. package/src/tui/dist/index.mjs +373 -81
  40. package/src/tui/engine/tui-steering-persist.mjs +66 -35
  41. package/src/tui/engine.mjs +6 -4
  42. package/src/tui/index.jsx +97 -6
  43. package/src/ui/statusline-segments.mjs +54 -36
  44. package/src/ui/statusline.mjs +141 -95
@@ -30,6 +30,14 @@ export { createSessionStats, applyUsageDelta } from './session-stats.mjs';
30
30
  export { contextPctDisplayLabel } from './statusline-format.mjs';
31
31
 
32
32
  const GATEWAY_QUOTA_STATUS_CACHE_MS = 500;
33
+ // Render-path sync-fs guard: loadGatewayStatus() / readCached*UsageSnapshot()
34
+ // below still read files synchronously (vendored/provider modules), but that
35
+ // work must never run on the 500ms render tick's own call stack. Both
36
+ // gateway-status and fallback-quota lookups below are stale-while-revalidate:
37
+ // the render call returns the last cached value immediately and defers the
38
+ // actual sync read to a separate macrotask (setImmediate), guarded so only
39
+ // one refresh per cache is ever in flight at a time. Visible cache cadence
40
+ // (500ms) is unchanged.
33
41
  const WORKER_SPINNER_FRAMES = Object.freeze(['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']);
34
42
  // L2 segment spinner: reuses the original WORKER_SPINNER_FRAMES dot glyphs (no
35
43
  // separate glyph list) but spins them at a faster 120ms step than the worker
@@ -42,7 +50,7 @@ const L2_SPINNER_FRAME_MS = 120;
42
50
  // replaces them. Snapshots from a previous launch stay hidden during boot so the
43
51
  // statusline starts empty until the current session captures usage once.
44
52
  const STATUSLINE_PROCESS_STARTED_AT_MS = Date.now() - Math.floor((Number(process.uptime?.()) || 0) * 1000);
45
- let _gatewayQuotaStatusCache = { key: '', at: 0, value: null };
53
+ let _gatewayQuotaStatusCache = { key: '', routeKey: '', at: 0, value: null };
46
54
  let _fallbackQuotaStatusCache = { key: '', at: 0, value: null };
47
55
  // Holds the last non-empty rendered L1 quota/usage segments per provider+route
48
56
  // key, but ONLY for providers that have actually armed the OAuth boot latch
@@ -94,6 +102,13 @@ function rememberNonEmptyQuotaSegments(key, segments) {
94
102
  // the new provider until ITS own confirmed snapshot exists — otherwise the new
95
103
  // provider's stale fallback balance (Credit $…) could leak prematurely.
96
104
  const _oauthUsageArmedProviders = new Set();
105
+ // Guards the background arm-check (readCachedOAuthUsageSnapshot sync read)
106
+ // so oauthUsageSegmentReady() never runs it inline on the render call stack,
107
+ // and so at most one in-flight check per provider is scheduled at a time.
108
+ const _oauthArmCheckInFlight = new Set();
109
+ // Guards the background fallbackQuotaStatus() refresh (sync snapshot reads)
110
+ // so at most one refresh is ever in flight across render ticks.
111
+ let _fallbackQuotaRefreshInFlight = false;
97
112
 
98
113
  function isConfirmedCurrentProcessSnapshot(snapshot) {
99
114
  if (!snapshot || typeof snapshot !== 'object') return false;
@@ -356,61 +371,76 @@ function renderNativeStatusline({
356
371
  return l2 ? `${l1}\n${l2}` : l1;
357
372
  }
358
373
 
374
+ let _gatewayQuotaRefreshInFlight = false;
375
+
359
376
  function loadGatewayQuotaStatus({
360
377
  provider, model, effort, fast, contextWindow, rawContextWindow, autoCompactTokenLimit = 0,
361
378
  sessionId, activeContextTokens, clientHostPid,
362
379
  } = {}) {
363
- const key = [
380
+ // Route identity: which route this cached value belongs to. Serving a stale
381
+ // value across a routeKey change would leak the previous provider/model's
382
+ // quota into the new route, so mismatches return null instead.
383
+ const routeKey = [
364
384
  String(provider || ''),
365
385
  String(model || ''),
366
386
  String(effort || ''),
367
387
  fast === true ? 'fast' : '',
368
388
  String(sessionId || ''),
369
389
  String(clientHostPid || ''),
390
+ ].join('\0');
391
+ // Freshness key: same route, but any of these changing should trigger a
392
+ // refresh (stale value still serveable meanwhile — same route identity).
393
+ const key = [
394
+ routeKey,
395
+ String(contextWindow ?? ''),
396
+ String(rawContextWindow ?? ''),
397
+ String(autoCompactTokenLimit ?? ''),
370
398
  Math.floor((Number(activeContextTokens) || 0) / 1024),
371
399
  ].join('\0');
372
400
  const now = Date.now();
373
- if (_gatewayQuotaStatusCache.key === key && now - _gatewayQuotaStatusCache.at < GATEWAY_QUOTA_STATUS_CACHE_MS) {
401
+ const fresh = _gatewayQuotaStatusCache.key === key && now - _gatewayQuotaStatusCache.at < GATEWAY_QUOTA_STATUS_CACHE_MS;
402
+ if (fresh) {
374
403
  return _gatewayQuotaStatusCache.value;
375
404
  }
376
- let value = null;
377
- try {
378
- const status = loadGatewayStatus({
379
- sessionId,
380
- activeContextTokens,
381
- clientHostPid,
382
- currentRoute: {
383
- provider,
384
- model,
385
- effort,
386
- fast,
387
- contextWindow,
388
- rawContextWindow,
389
- autoCompactTokenLimit,
390
- },
405
+ // Stale-while-revalidate: serve the last cached value for THIS render call
406
+ // only if it belongs to the SAME route, and kick a background refresh off
407
+ // the render call stack. Guarded so concurrent render ticks never queue
408
+ // more than one refresh at a time.
409
+ if (!_gatewayQuotaRefreshInFlight) {
410
+ _gatewayQuotaRefreshInFlight = true;
411
+ setImmediate(() => {
412
+ let value = null;
413
+ try {
414
+ const status = loadGatewayStatus({
415
+ sessionId,
416
+ activeContextTokens,
417
+ clientHostPid,
418
+ currentRoute: {
419
+ provider,
420
+ model,
421
+ effort,
422
+ fast,
423
+ contextWindow,
424
+ rawContextWindow,
425
+ autoCompactTokenLimit,
426
+ },
427
+ });
428
+ const statusProvider = String(status?.provider || '').trim();
429
+ const cliProvider = String(provider || '').trim();
430
+ const statusModel = String(status?.model || '').trim();
431
+ const cliModel = String(model || '').trim();
432
+ if (status && !(cliProvider && statusProvider && statusProvider !== cliProvider)
433
+ && !(cliModel && statusModel && statusModel !== cliModel)) {
434
+ value = status;
435
+ }
436
+ } catch {
437
+ value = null;
438
+ }
439
+ _gatewayQuotaStatusCache = { key, routeKey, at: Date.now(), value };
440
+ _gatewayQuotaRefreshInFlight = false;
391
441
  });
392
- if (!status) {
393
- _gatewayQuotaStatusCache = { key, at: now, value };
394
- return value;
395
- }
396
- const statusProvider = String(status.provider || '').trim();
397
- const cliProvider = String(provider || '').trim();
398
- if (cliProvider && statusProvider && statusProvider !== cliProvider) {
399
- _gatewayQuotaStatusCache = { key, at: now, value };
400
- return value;
401
- }
402
- const statusModel = String(status.model || '').trim();
403
- const cliModel = String(model || '').trim();
404
- if (cliModel && statusModel && statusModel !== cliModel) {
405
- _gatewayQuotaStatusCache = { key, at: now, value };
406
- return value;
407
- }
408
- value = status;
409
- } catch {
410
- value = null;
411
442
  }
412
- _gatewayQuotaStatusCache = { key, at: now, value };
413
- return value;
443
+ return _gatewayQuotaStatusCache.routeKey === routeKey ? _gatewayQuotaStatusCache.value : null;
414
444
  }
415
445
 
416
446
  // Option A boot gate. Returns true once THIS process has captured its first
@@ -428,19 +458,27 @@ function oauthUsageSegmentReady({ provider, model } = {}) {
428
458
  const normalizedProvider = String(provider || '').trim().toLowerCase();
429
459
  if (!normalizedProvider.includes('oauth')) return true;
430
460
  if (_oauthUsageArmedProviders.has(normalizedProvider)) return true;
431
- let snapshot = null;
432
- try {
433
- snapshot = readCachedOAuthUsageSnapshot({
434
- provider: normalizedProvider,
435
- model: String(model || '').trim(),
436
- providerKind: providerKindForQuota(normalizedProvider),
437
- }, { allowStale: true });
438
- } catch {
439
- snapshot = null;
440
- }
441
- if (isConfirmedCurrentProcessSnapshot(snapshot)) {
442
- _oauthUsageArmedProviders.add(normalizedProvider);
443
- return true;
461
+ // Not yet armed: never do the sync snapshot read on this render call. Kick
462
+ // one background check (guarded per-provider) and keep returning false
463
+ // (today's pre-arm behavior) until it flips the latch.
464
+ if (!_oauthArmCheckInFlight.has(normalizedProvider)) {
465
+ _oauthArmCheckInFlight.add(normalizedProvider);
466
+ setImmediate(() => {
467
+ try {
468
+ const snapshot = readCachedOAuthUsageSnapshot({
469
+ provider: normalizedProvider,
470
+ model: String(model || '').trim(),
471
+ providerKind: providerKindForQuota(normalizedProvider),
472
+ }, { allowStale: true });
473
+ if (isConfirmedCurrentProcessSnapshot(snapshot)) {
474
+ _oauthUsageArmedProviders.add(normalizedProvider);
475
+ }
476
+ } catch {
477
+ /* stay unarmed; next tick retries */
478
+ } finally {
479
+ _oauthArmCheckInFlight.delete(normalizedProvider);
480
+ }
481
+ });
444
482
  }
445
483
  return false;
446
484
  }
@@ -453,50 +491,58 @@ function fallbackQuotaStatus({ provider, model } = {}) {
453
491
  if (_fallbackQuotaStatusCache.key === cacheKey && cacheNow - _fallbackQuotaStatusCache.at < GATEWAY_QUOTA_STATUS_CACHE_MS) {
454
492
  return _fallbackQuotaStatusCache.value;
455
493
  }
456
- const routeInfo = {
457
- provider: normalizedProvider,
458
- model: String(model || '').trim(),
459
- providerKind: providerKindForQuota(normalizedProvider),
460
- };
461
- let usageSnapshot = null;
462
- if (normalizedProvider === 'opencode-go') {
463
- usageSnapshot = readCachedOpenCodeGoUsageSnapshot();
464
- if (!usageSnapshot) {
465
- _fallbackQuotaStatusCache = { key: cacheKey, at: cacheNow, value: null };
466
- return null;
467
- }
468
- } else if (normalizedProvider.includes('oauth')) {
469
- try {
470
- usageSnapshot = readCachedOAuthUsageSnapshot(routeInfo, { allowStale: true });
471
- } catch {}
472
- }
473
- // Boot guard: do not render previous-launch usage before the current runtime
474
- // has captured at least one snapshot. Once a snapshot is captured in this
475
- // process, keep it visible while idle even if refreshes are delayed.
476
- if (usageSnapshot) {
477
- const cachedAt = num(usageSnapshot.cachedAt, 0);
478
- if (!cachedAt || cachedAt < STATUSLINE_PROCESS_STARTED_AT_MS) {
479
- usageSnapshot = { ...usageSnapshot, quotaWindows: [] };
480
- }
481
- }
482
- try {
483
- const limits = buildGatewayLimits(routeInfo, null, usageSnapshot);
484
- if (!limits?.quotaWindows?.length && !limits?.balance && !limits?.routeSpend) {
485
- _fallbackQuotaStatusCache = { key: cacheKey, at: cacheNow, value: null };
486
- return null;
487
- }
488
- const value = {
489
- ...routeInfo,
490
- quotaWindows: limits.quotaWindows || [],
491
- balance: limits.balance || null,
492
- routeSpend: limits.routeSpend || null,
493
- };
494
- _fallbackQuotaStatusCache = { key: cacheKey, at: cacheNow, value };
495
- return value;
496
- } catch {
497
- _fallbackQuotaStatusCache = { key: cacheKey, at: cacheNow, value: null };
498
- return null;
494
+ // Stale-while-revalidate: serve last cached value for this render call
495
+ // (same provider+model only — a route switch must not leak the previous
496
+ // route's balance/spend), refresh (sync snapshot reads included) off the
497
+ // render call stack.
498
+ if (!_fallbackQuotaRefreshInFlight) {
499
+ _fallbackQuotaRefreshInFlight = true;
500
+ setImmediate(() => {
501
+ const routeInfo = {
502
+ provider: normalizedProvider,
503
+ model: String(model || '').trim(),
504
+ providerKind: providerKindForQuota(normalizedProvider),
505
+ };
506
+ let value = null;
507
+ try {
508
+ let usageSnapshot = null;
509
+ if (normalizedProvider === 'opencode-go') {
510
+ usageSnapshot = readCachedOpenCodeGoUsageSnapshot();
511
+ } else if (normalizedProvider.includes('oauth')) {
512
+ try {
513
+ usageSnapshot = readCachedOAuthUsageSnapshot(routeInfo, { allowStale: true });
514
+ } catch {}
515
+ }
516
+ if (normalizedProvider === 'opencode-go' && !usageSnapshot) {
517
+ value = null;
518
+ } else {
519
+ // Boot guard: do not render previous-launch usage before the current
520
+ // runtime has captured at least one snapshot. Once captured in this
521
+ // process, keep it visible while idle even if refreshes are delayed.
522
+ if (usageSnapshot) {
523
+ const cachedAt = num(usageSnapshot.cachedAt, 0);
524
+ if (!cachedAt || cachedAt < STATUSLINE_PROCESS_STARTED_AT_MS) {
525
+ usageSnapshot = { ...usageSnapshot, quotaWindows: [] };
526
+ }
527
+ }
528
+ const limits = buildGatewayLimits(routeInfo, null, usageSnapshot);
529
+ if (limits?.quotaWindows?.length || limits?.balance || limits?.routeSpend) {
530
+ value = {
531
+ ...routeInfo,
532
+ quotaWindows: limits.quotaWindows || [],
533
+ balance: limits.balance || null,
534
+ routeSpend: limits.routeSpend || null,
535
+ };
536
+ }
537
+ }
538
+ } catch {
539
+ value = null;
540
+ }
541
+ _fallbackQuotaStatusCache = { key: cacheKey, at: Date.now(), value };
542
+ _fallbackQuotaRefreshInFlight = false;
543
+ });
499
544
  }
545
+ return _fallbackQuotaStatusCache.key === cacheKey ? _fallbackQuotaStatusCache.value : null;
500
546
  }
501
547
 
502
548
  function providerKindForQuota(provider) {