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.
- package/package.json +2 -1
- package/scripts/atomic-lock-tryonce-test.mjs +66 -0
- package/scripts/bench/cache-probe-tasks.json +1 -1
- package/scripts/bench/lead-review-tasks-r3.json +1 -1
- package/scripts/bench/r4-mixed-tasks.json +1 -1
- package/scripts/bench/review-tasks.json +1 -1
- package/scripts/build-runtime-windows.ps1 +242 -242
- package/scripts/provider-toolcall-test.mjs +79 -2
- package/scripts/recall-usecase-cases.json +1 -1
- package/scripts/smoke-runtime-negative.ps1 +106 -106
- package/scripts/tool-efficiency-diag.mjs +1 -1
- package/src/mixdog-session-runtime.mjs +12 -0
- package/src/rules/lead/02-channels.md +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +33 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +32 -18
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +142 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +108 -30
- package/src/runtime/agent/orchestrator/session/store.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +22 -12
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +15 -15
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +10 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +8 -2
- package/src/runtime/channels/lib/runtime-paths.mjs +8 -19
- package/src/runtime/memory/index.mjs +37 -0
- package/src/runtime/memory/lib/embedding-warmup.mjs +3 -0
- package/src/runtime/memory/lib/ko-morph.mjs +1 -0
- package/src/runtime/shared/atomic-file.mjs +110 -0
- package/src/runtime/shared/transcript-writer.mjs +46 -4
- package/src/session-runtime/provider-models.mjs +47 -8
- package/src/tui/app/transcript-window.mjs +115 -6
- package/src/tui/app/use-transcript-window.mjs +58 -7
- package/src/tui/components/StatusLine.jsx +1 -1
- package/src/tui/dist/index.mjs +373 -81
- package/src/tui/engine/tui-steering-persist.mjs +66 -35
- package/src/tui/engine.mjs +6 -4
- package/src/tui/index.jsx +97 -6
- package/src/ui/statusline-segments.mjs +54 -36
- package/src/ui/statusline.mjs +141 -95
package/src/ui/statusline.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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) {
|