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
@@ -3516,7 +3516,7 @@ function hasActiveStatuslineTools(activeTools = null) {
3516
3516
  return e || s;
3517
3517
  }
3518
3518
  function hasActiveStatuslineWork(line, agentWorkers = [], agentJobs = [], activeTools = null) {
3519
- return hasRunningStatuslineWorkers(agentWorkers, agentJobs) || hasActiveStatuslineTools(activeTools) || /\bRunning \d+ (?:Agents?|Shells?)\b/.test(stripAnsi(line)) || /\b(?:Exploring|Searching)\b/.test(stripAnsi(line));
3519
+ return hasRunningStatuslineWorkers(agentWorkers, agentJobs) || hasActiveStatuslineTools(activeTools) || /\bRunning \d+ (?:Agents?|Shells?)\b/.test(stripAnsi(line)) || /\b(?:Exploring|Searching|Memory)\b/.test(stripAnsi(line));
3520
3520
  }
3521
3521
  function bootFullRenderEligible(mountAtMs, line, agentWorkers = [], agentJobs = [], activeTools = null) {
3522
3522
  const elapsed = Date.now() - mountAtMs;
@@ -7538,6 +7538,12 @@ function withFileLockSync(lockPath, fn, opts = {}) {
7538
7538
  } catch (err) {
7539
7539
  lastErr = err;
7540
7540
  if (!LOCK_WAIT_CODES.has(err?.code)) throw err;
7541
+ if (timeoutMs <= 0) {
7542
+ const contErr = new Error(`atomic lock contended (try-once): ${lockPath}`);
7543
+ contErr.code = "ELOCKCONTENDED";
7544
+ contErr.cause = err;
7545
+ throw contErr;
7546
+ }
7541
7547
  try {
7542
7548
  const st = statSync3(lockPath);
7543
7549
  if (Date.now() - st.mtimeMs > staleMs) {
@@ -7694,6 +7700,91 @@ function _releaseReclaimGuard(reclaim) {
7694
7700
  }
7695
7701
  }
7696
7702
  }
7703
+ function _asyncSleep(ms) {
7704
+ return new Promise((resolve5) => {
7705
+ setTimeout(resolve5, Math.max(1, Number(ms) || 1));
7706
+ });
7707
+ }
7708
+ async function withFileLock(lockPath, fn, opts = {}) {
7709
+ const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : DEFAULT_LOCK_TIMEOUT_MS;
7710
+ const staleMs = Number.isFinite(opts.staleMs) ? opts.staleMs : 3e4;
7711
+ const deadline = Date.now() + timeoutMs;
7712
+ mkdirSync2(dirname3(lockPath), { recursive: true });
7713
+ let attempt = 0;
7714
+ let lastErr = null;
7715
+ while (true) {
7716
+ let fd;
7717
+ try {
7718
+ fd = openSync(lockPath, "wx");
7719
+ } catch (err) {
7720
+ lastErr = err;
7721
+ if (!LOCK_WAIT_CODES.has(err?.code)) throw err;
7722
+ if (timeoutMs <= 0) {
7723
+ const contErr = new Error(`atomic lock contended (try-once): ${lockPath}`);
7724
+ contErr.code = "ELOCKCONTENDED";
7725
+ contErr.cause = err;
7726
+ throw contErr;
7727
+ }
7728
+ try {
7729
+ const st = statSync3(lockPath);
7730
+ if (Date.now() - st.mtimeMs > staleMs) {
7731
+ const deadPid = _readLockOwnerPid(lockPath);
7732
+ if (deadPid !== null && _pidIsDead(deadPid)) {
7733
+ const reclaim = _tryAcquireReclaimGuard(lockPath, staleMs);
7734
+ if (reclaim !== null) {
7735
+ let reclaimed = false;
7736
+ try {
7737
+ const currentSt = statSync3(lockPath);
7738
+ if (Date.now() - currentSt.mtimeMs > staleMs) {
7739
+ const currentPid = _readLockOwnerPid(lockPath);
7740
+ if (currentPid === deadPid && _pidIsDead(currentPid)) {
7741
+ try {
7742
+ unlinkSync2(lockPath);
7743
+ reclaimed = true;
7744
+ } catch {
7745
+ }
7746
+ }
7747
+ }
7748
+ } finally {
7749
+ _releaseReclaimGuard(reclaim);
7750
+ }
7751
+ if (reclaimed) continue;
7752
+ }
7753
+ }
7754
+ }
7755
+ } catch {
7756
+ }
7757
+ if (Date.now() >= deadline) break;
7758
+ const base = DEFAULT_BACKOFFS_MS[Math.min(attempt, DEFAULT_BACKOFFS_MS.length - 1)];
7759
+ const jitter = Math.floor(Math.random() * Math.min(75, Math.max(1, base)));
7760
+ await _asyncSleep(Math.min(Math.max(1, deadline - Date.now()), base + jitter));
7761
+ attempt += 1;
7762
+ continue;
7763
+ }
7764
+ try {
7765
+ writeFileSync2(fd, `${process.pid} ${Date.now()}
7766
+ `, "utf8");
7767
+ } catch {
7768
+ }
7769
+ try {
7770
+ if (opts.secret === true) _enforceOwnerOnlyAclWin32(lockPath);
7771
+ return await fn();
7772
+ } finally {
7773
+ try {
7774
+ closeSync(fd);
7775
+ } catch {
7776
+ }
7777
+ try {
7778
+ if (_lockOwnedByPid(lockPath, process.pid)) unlinkSync2(lockPath);
7779
+ } catch {
7780
+ }
7781
+ }
7782
+ }
7783
+ const timeoutErr = new Error(`atomic lock timeout after ${timeoutMs}ms: ${lockPath}`);
7784
+ timeoutErr.code = "ELOCKTIMEOUT";
7785
+ timeoutErr.cause = lastErr;
7786
+ throw timeoutErr;
7787
+ }
7697
7788
  var _cachedUserSid = null;
7698
7789
  function _resolveCurrentUserPrincipal() {
7699
7790
  const systemRoot = process.env.SystemRoot || process.env.windir;
@@ -7854,6 +7945,21 @@ function updateJsonAtomicSync(filePath, mutator, opts = {}) {
7854
7945
  return next;
7855
7946
  }, opts);
7856
7947
  }
7948
+ async function updateJsonAtomic(filePath, mutator, opts = {}) {
7949
+ const { lock: _lock, ...writeOpts } = opts;
7950
+ return withFileLock(`${filePath}.lock`, () => {
7951
+ let cur = null;
7952
+ try {
7953
+ cur = JSON.parse(readFileSync3(filePath, "utf8"));
7954
+ } catch {
7955
+ cur = null;
7956
+ }
7957
+ const next = mutator(cur);
7958
+ if (next === void 0) return cur;
7959
+ writeJsonAtomicSync(filePath, next, { ...writeOpts, lock: false });
7960
+ return next;
7961
+ }, opts);
7962
+ }
7857
7963
 
7858
7964
  // src/runtime/shared/user-data-guard.mjs
7859
7965
  import {
@@ -11007,14 +11113,13 @@ function estimateTranscriptItemRowsCached(item, columns, toolOutputExpanded) {
11007
11113
  if (!item) return Math.max(1, Math.ceil(estimateTranscriptItemRows(item, columns, toolOutputExpanded)));
11008
11114
  if (shouldSuppressFullyFailedToolItem(item)) return 0;
11009
11115
  if (item.kind === "assistant" && item.streaming) {
11010
- const estimate = streamingEstimateRows(item, columns, toolOutputExpanded);
11011
11116
  const toolExpanded2 = toolOutputExpanded ? 1 : 0;
11012
11117
  const idEntry = streamingMeasuredRowsById.get(item.id);
11013
11118
  if (idEntry && idEntry.rows > 0 && idEntry.columns === columns && idEntry.toolExpanded === toolExpanded2) {
11014
- return Math.max(idEntry.rows, estimate);
11119
+ return idEntry.rows;
11015
11120
  }
11016
11121
  if (idEntry) streamingMeasuredRowsById.delete(item.id);
11017
- return estimate;
11122
+ return streamingEstimateRows(item, columns, toolOutputExpanded);
11018
11123
  }
11019
11124
  if (item.kind === "assistant" && streamingMeasuredRowsById.has(item.id)) {
11020
11125
  streamingMeasuredRowsById.delete(item.id);
@@ -11047,6 +11152,61 @@ function buildTranscriptRowIndex(items, {
11047
11152
  }
11048
11153
  return { rows, prefixRows, totalRows: prefixRows[allItems.length] || 0 };
11049
11154
  }
11155
+ function trailingStreamingItem(allItems) {
11156
+ const last = allItems.length > 0 ? allItems[allItems.length - 1] : null;
11157
+ return last && last.kind === "assistant" && last.streaming ? last : null;
11158
+ }
11159
+ function buildTranscriptRowIndexIncremental(items, {
11160
+ columns = 80,
11161
+ toolOutputExpanded = false,
11162
+ suppressMeasuredRowHeights = false,
11163
+ measuredRowsVersion = 0,
11164
+ cacheRef = null,
11165
+ prefixSig = null
11166
+ } = {}) {
11167
+ const allItems = Array.isArray(items) ? items : [];
11168
+ const tail = trailingStreamingItem(allItems);
11169
+ const holder = cacheRef || { current: null };
11170
+ if (!tail) {
11171
+ holder.current = null;
11172
+ return buildTranscriptRowIndex(allItems, {
11173
+ columns,
11174
+ toolOutputExpanded,
11175
+ suppressMeasuredRowHeights
11176
+ });
11177
+ }
11178
+ const prefixLen = allItems.length - 1;
11179
+ const cache = holder.current;
11180
+ if (cache && cache.columns === columns && cache.toolExpanded === (toolOutputExpanded ? 1 : 0) && cache.suppress === suppressMeasuredRowHeights && cache.version === measuredRowsVersion && cache.prefixLen === prefixLen && prefixSig != null && cache.prefixSig === prefixSig && cache.tailId === tail.id) {
11181
+ {
11182
+ const tailMeasured = suppressMeasuredRowHeights ? null : measuredTranscriptRows(tail, columns, toolOutputExpanded);
11183
+ const tailRows = tailMeasured != null ? tailMeasured : estimateTranscriptItemRowsCached(tail, columns, toolOutputExpanded);
11184
+ const rows = cache.prefixRowsArr.slice();
11185
+ rows.push(tailRows);
11186
+ const prefixRows = cache.prefixPrefixRows.slice();
11187
+ prefixRows.push(cache.prefixTotal + tailRows);
11188
+ return { rows, prefixRows, totalRows: prefixRows[allItems.length] || 0 };
11189
+ }
11190
+ }
11191
+ const full = buildTranscriptRowIndex(allItems, {
11192
+ columns,
11193
+ toolOutputExpanded,
11194
+ suppressMeasuredRowHeights
11195
+ });
11196
+ holder.current = {
11197
+ columns,
11198
+ toolExpanded: toolOutputExpanded ? 1 : 0,
11199
+ suppress: suppressMeasuredRowHeights,
11200
+ version: measuredRowsVersion,
11201
+ prefixLen,
11202
+ prefixSig,
11203
+ tailId: tail.id,
11204
+ prefixRowsArr: full.rows.slice(0, prefixLen),
11205
+ prefixPrefixRows: full.prefixRows.slice(0, prefixLen + 1),
11206
+ prefixTotal: full.prefixRows[prefixLen] || 0
11207
+ };
11208
+ return full;
11209
+ }
11050
11210
  var transcriptSigPartCache = /* @__PURE__ */ new WeakMap();
11051
11211
  function transcriptStructureSignature(items, columns, toolOutputExpanded) {
11052
11212
  const list = Array.isArray(items) ? items : [];
@@ -11880,6 +12040,7 @@ function useTranscriptWindow({
11880
12040
  }) {
11881
12041
  const transcriptTotalRowsRef = useRef8(0);
11882
12042
  const preservedScrollDeltaRef = useRef8(0);
12043
+ const incrementalRowIndexCacheRef = useRef8(null);
11883
12044
  const prevViewportGeomRef = useRef8({ contentHeight: 0, floatingPanelRows: 0 });
11884
12045
  const transcriptItemElsRef = useRef8(/* @__PURE__ */ new Map());
11885
12046
  const transcriptMeasureRefCache = useRef8(/* @__PURE__ */ new Map());
@@ -11907,10 +12068,21 @@ function useTranscriptWindow({
11907
12068
  }
11908
12069
  return fn;
11909
12070
  }, []);
11910
- const transcriptStructureSig = useMemo(
11911
- () => transcriptStructureSignature(items, frameColumns, toolOutputExpanded),
11912
- [items, frameColumns, toolOutputExpanded]
12071
+ const streamingTailItem = useMemo(() => {
12072
+ const last = (items || []).length > 0 ? items[items.length - 1] : null;
12073
+ return last && last.kind === "assistant" && last.streaming ? last : null;
12074
+ }, [items]);
12075
+ const prefixLen = streamingTailItem ? (items || []).length - 1 : (items || []).length;
12076
+ const prefixSig = useMemo(
12077
+ () => transcriptStructureSignature(
12078
+ streamingTailItem ? (items || []).slice(0, prefixLen) : items || [],
12079
+ frameColumns,
12080
+ toolOutputExpanded
12081
+ ),
12082
+ [items, streamingTailItem, prefixLen, frameColumns, toolOutputExpanded]
11913
12083
  );
12084
+ const tailSig = streamingTailItem ? `a${streamingTailItem.id}:${estimateTranscriptItemRowsCached(streamingTailItem, frameColumns, toolOutputExpanded)}` : "_";
12085
+ const transcriptStructureSig = `${prefixSig}#${tailSig}`;
11914
12086
  const transcriptStreamingActive = (items || []).some(
11915
12087
  (item) => item?.kind === "assistant" && item?.streaming
11916
12088
  );
@@ -11921,12 +12093,15 @@ function useTranscriptWindow({
11921
12093
  const hasStreamingReadingAnchor = !!transcriptAnchorRef.current || transcriptAnchorDirtyRef.current;
11922
12094
  const bottomPinnedForMeasure = transcriptPinnedForStreaming;
11923
12095
  const suppressMeasuredRowHeights = bottomPinnedForMeasure || transcriptStreamingActive && (scrolledUpForStreamingMeasure && hasStreamingReadingAnchor || scrolledUpForStreamingMeasure && prevEstimateGeometry && !transcriptPinnedForStreaming);
11924
- const transcriptRowIndex = useMemo(() => buildTranscriptRowIndex(items, {
12096
+ const transcriptRowIndex = useMemo(() => buildTranscriptRowIndexIncremental(items, {
11925
12097
  columns: frameColumns,
11926
12098
  toolOutputExpanded,
11927
- suppressMeasuredRowHeights
11928
- // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: sig captures the relevant item changes; measuredRowsVersion folds in app-level measured height corrections
11929
- }), [transcriptStructureSig, measuredRowsVersion, suppressMeasuredRowHeights]);
12099
+ suppressMeasuredRowHeights,
12100
+ measuredRowsVersion,
12101
+ cacheRef: incrementalRowIndexCacheRef,
12102
+ prefixSig
12103
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: prefixSig/tailSig capture the relevant item changes (raw `items` dropped so a tail-only flush never re-enters the builder via array identity); measuredRowsVersion folds in app-level measured height corrections
12104
+ }), [prefixSig, tailSig, measuredRowsVersion, suppressMeasuredRowHeights]);
11930
12105
  const hasReadingAnchor = !!transcriptAnchorRef.current && !transcriptAnchorDirtyRef.current;
11931
12106
  const scrolledUpRows = Math.max(0, Number(scrollTargetRef.current) || 0);
11932
12107
  const scrolledUp = scrolledUpRows > transcriptBottomSlackRows;
@@ -22149,6 +22324,13 @@ import { randomBytes as randomBytes3 } from "crypto";
22149
22324
  import { join as join7 } from "path";
22150
22325
  var PENDING_MESSAGES_FILE = "session-pending-messages.json";
22151
22326
  var PENDING_MESSAGES_MODE = 384;
22327
+ var _persistChain = Promise.resolve();
22328
+ function _serialize(task) {
22329
+ const run = _persistChain.then(task, task);
22330
+ _persistChain = run.catch(() => {
22331
+ });
22332
+ return run;
22333
+ }
22152
22334
  function pendingMessagesPath() {
22153
22335
  return join7(resolvePluginData(), PENDING_MESSAGES_FILE);
22154
22336
  }
@@ -22216,59 +22398,67 @@ function removePersistRow(q, entry) {
22216
22398
  }
22217
22399
  function appendTuiSteeringPersist(leadSessionId, entry) {
22218
22400
  const text = entryPersistText(entry);
22219
- if (!text) return;
22401
+ if (!text) return Promise.resolve();
22220
22402
  const key = tuiSteeringSessionKey(leadSessionId);
22221
- if (!key) return;
22403
+ if (!key) return Promise.resolve();
22222
22404
  if (!entry.steeringPersistId) entry.steeringPersistId = newSteeringPersistId();
22223
22405
  const record = { id: entry.steeringPersistId, text };
22224
- try {
22225
- updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
22226
- const next = normalizePendingStore(raw);
22227
- const q = Array.isArray(next.sessions[key]) ? next.sessions[key] : [];
22228
- q.push(record);
22229
- next.sessions[key] = q;
22230
- const now = Date.now();
22231
- next.updatedAt = now;
22232
- touchPendingSessionEntry(next, key, now);
22233
- return next;
22234
- }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
22235
- } catch (err) {
22406
+ return _serialize(async () => {
22236
22407
  try {
22237
- process.stderr.write(`[tui] steering-queue append failed sessionId=${leadSessionId}: ${err?.message || err}
22408
+ await updateJsonAtomic(pendingMessagesPath(), (raw) => {
22409
+ const next = normalizePendingStore(raw);
22410
+ const q = Array.isArray(next.sessions[key]) ? next.sessions[key] : [];
22411
+ q.push(record);
22412
+ next.sessions[key] = q;
22413
+ const now = Date.now();
22414
+ next.updatedAt = now;
22415
+ touchPendingSessionEntry(next, key, now);
22416
+ return next;
22417
+ }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
22418
+ } catch (err) {
22419
+ try {
22420
+ process.stderr.write(`[tui] steering-queue append failed sessionId=${leadSessionId}: ${err?.message || err}
22238
22421
  `);
22239
- } catch {
22422
+ } catch {
22423
+ }
22240
22424
  }
22241
- }
22425
+ });
22242
22426
  }
22243
22427
  function dropTuiSteeringPersist(leadSessionId, entries) {
22244
22428
  const key = tuiSteeringSessionKey(leadSessionId);
22245
- if (!key) return;
22429
+ if (!key) return Promise.resolve();
22246
22430
  const batch = Array.isArray(entries) ? entries : [];
22247
- if (batch.length === 0) return;
22248
- try {
22249
- updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
22250
- const next = normalizePendingStore(raw);
22251
- const q = Array.isArray(next.sessions[key]) ? next.sessions[key].slice() : [];
22252
- if (q.length === 0) return void 0;
22253
- for (const entry of batch) {
22254
- removePersistRow(q, entry);
22255
- }
22256
- if (q.length === 0) {
22257
- delete next.sessions[key];
22258
- if (next.sessionTouchedAt) delete next.sessionTouchedAt[key];
22259
- } else {
22260
- next.sessions[key] = q;
22261
- }
22262
- next.updatedAt = Date.now();
22263
- return next;
22264
- }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
22265
- } catch (err) {
22431
+ if (batch.length === 0) return Promise.resolve();
22432
+ return _serialize(async () => {
22266
22433
  try {
22267
- process.stderr.write(`[tui] steering-queue drop failed sessionId=${leadSessionId}: ${err?.message || err}
22434
+ await updateJsonAtomic(pendingMessagesPath(), (raw) => {
22435
+ const next = normalizePendingStore(raw);
22436
+ const q = Array.isArray(next.sessions[key]) ? next.sessions[key].slice() : [];
22437
+ if (q.length === 0) return void 0;
22438
+ for (const entry of batch) {
22439
+ removePersistRow(q, entry);
22440
+ }
22441
+ if (q.length === 0) {
22442
+ delete next.sessions[key];
22443
+ if (next.sessionTouchedAt) delete next.sessionTouchedAt[key];
22444
+ } else {
22445
+ next.sessions[key] = q;
22446
+ }
22447
+ next.updatedAt = Date.now();
22448
+ return next;
22449
+ }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
22450
+ } catch (err) {
22451
+ try {
22452
+ process.stderr.write(`[tui] steering-queue drop failed sessionId=${leadSessionId}: ${err?.message || err}
22268
22453
  `);
22269
- } catch {
22454
+ } catch {
22455
+ }
22270
22456
  }
22271
- }
22457
+ });
22458
+ }
22459
+ function flushTuiSteeringPersist() {
22460
+ return _persistChain.catch(() => {
22461
+ });
22272
22462
  }
22273
22463
  function drainedRowToRestore(row) {
22274
22464
  if (typeof row === "string") {
@@ -22281,27 +22471,29 @@ function drainedRowToRestore(row) {
22281
22471
  }
22282
22472
  function drainTuiSteeringPersist(leadSessionId) {
22283
22473
  const key = tuiSteeringSessionKey(leadSessionId);
22284
- if (!key) return [];
22285
- let drained = [];
22286
- try {
22287
- updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
22288
- const next = normalizePendingStore(raw);
22289
- const q = Array.isArray(next.sessions[key]) ? next.sessions[key] : [];
22290
- drained = q.map(drainedRowToRestore).filter(Boolean);
22291
- if (drained.length === 0) return void 0;
22292
- delete next.sessions[key];
22293
- if (next.sessionTouchedAt) delete next.sessionTouchedAt[key];
22294
- next.updatedAt = Date.now();
22295
- return next;
22296
- }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
22297
- } catch (err) {
22474
+ if (!key) return Promise.resolve([]);
22475
+ return _serialize(async () => {
22476
+ let drained = [];
22298
22477
  try {
22299
- process.stderr.write(`[tui] steering-queue drain failed sessionId=${leadSessionId}: ${err?.message || err}
22478
+ await updateJsonAtomic(pendingMessagesPath(), (raw) => {
22479
+ const next = normalizePendingStore(raw);
22480
+ const q = Array.isArray(next.sessions[key]) ? next.sessions[key] : [];
22481
+ drained = q.map(drainedRowToRestore).filter(Boolean);
22482
+ if (drained.length === 0) return void 0;
22483
+ delete next.sessions[key];
22484
+ if (next.sessionTouchedAt) delete next.sessionTouchedAt[key];
22485
+ next.updatedAt = Date.now();
22486
+ return next;
22487
+ }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
22488
+ } catch (err) {
22489
+ try {
22490
+ process.stderr.write(`[tui] steering-queue drain failed sessionId=${leadSessionId}: ${err?.message || err}
22300
22491
  `);
22301
- } catch {
22492
+ } catch {
22493
+ }
22302
22494
  }
22303
- }
22304
- return drained;
22495
+ return drained;
22496
+ });
22305
22497
  }
22306
22498
 
22307
22499
  // src/tui/engine.mjs
@@ -23638,8 +23830,8 @@ async function createEngineSession({
23638
23830
  commitSteeringQueueEntries(batch);
23639
23831
  return out;
23640
23832
  }
23641
- function restoreLeadSteeringFromDisk() {
23642
- const rows = drainTuiSteeringPersist(leadSessionId());
23833
+ async function restoreLeadSteeringFromDisk() {
23834
+ const rows = await drainTuiSteeringPersist(leadSessionId());
23643
23835
  if (!rows.length) return;
23644
23836
  const restored = [];
23645
23837
  for (const row of rows) {
@@ -23820,7 +24012,7 @@ async function createEngineSession({
23820
24012
  syncContextStats({ allowEstimated: true });
23821
24013
  return state.stats;
23822
24014
  };
23823
- restoreLeadSteeringFromDisk();
24015
+ void restoreLeadSteeringFromDisk();
23824
24016
  return {
23825
24017
  getState: () => state,
23826
24018
  patchItem,
@@ -24743,7 +24935,7 @@ async function createEngineSession({
24743
24935
  ...routeState(),
24744
24936
  stats: { ...state.stats }
24745
24937
  });
24746
- restoreLeadSteeringFromDisk();
24938
+ void restoreLeadSteeringFromDisk();
24747
24939
  return true;
24748
24940
  } finally {
24749
24941
  set({ commandBusy: false, commandStatus: null });
@@ -24768,6 +24960,7 @@ async function createEngineSession({
24768
24960
  }
24769
24961
  unsubscribeRemoteState = null;
24770
24962
  denyAllToolApprovals("runtime closing");
24963
+ await flushTuiSteeringPersist();
24771
24964
  await runtime.close(reason, options);
24772
24965
  listeners.clear();
24773
24966
  }
@@ -24953,7 +25146,7 @@ function touchUiHeartbeat(instanceId) {
24953
25146
  if (!curRaw) return void 0;
24954
25147
  if (instanceId && curRaw.instanceId !== instanceId) return void 0;
24955
25148
  return { ...curRaw, ui_heartbeat_at: Date.now(), updatedAt: Date.now() };
24956
- }, { compact: true, fsync: false, fsyncDir: false, renameFallback: "truncate" });
25149
+ }, { compact: true, fsync: false, fsyncDir: false, renameFallback: "truncate", timeoutMs: 0 });
24957
25150
  } catch {
24958
25151
  }
24959
25152
  }
@@ -24975,36 +25168,129 @@ var EXIT_HARD_DELAY_MS = positiveIntEnv2("MIXDOG_TUI_HARD_EXIT_DELAY_MS", 500);
24975
25168
  var EXIT_HARD_ENABLED = !/^(0|false|no|off)$/i.test(String(process.env.MIXDOG_TUI_HARD_EXIT || "1"));
24976
25169
  var EXIT_DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_TUI_EXIT_DEBUG || ""));
24977
25170
  var PERF_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_TUI_PERF || ""));
25171
+ var LOOP_PROBE_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_TUI_LOOP_PROBE || ""));
25172
+ var LOOP_PROBE_INTERVAL_MS = 1e3;
25173
+ var LOOP_PROBE_DRIFT_THRESHOLD_MS = 200;
24978
25174
  function positiveIntEnv2(name, fallback) {
24979
25175
  const value = Number(process.env[name]);
24980
25176
  return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
24981
25177
  }
25178
+ function installTuiLoopProbe() {
25179
+ if (!LOOP_PROBE_ENABLED) return () => {
25180
+ };
25181
+ let last = performance3.now();
25182
+ const timer = setInterval(() => {
25183
+ const now = performance3.now();
25184
+ const drift = now - last - LOOP_PROBE_INTERVAL_MS;
25185
+ last = now;
25186
+ if (drift > LOOP_PROBE_DRIFT_THRESHOLD_MS) {
25187
+ try {
25188
+ process.stderr.write(`[mixdog-loop] stall=${drift.toFixed(0)}ms
25189
+ `);
25190
+ } catch {
25191
+ }
25192
+ }
25193
+ }, LOOP_PROBE_INTERVAL_MS);
25194
+ timer.unref?.();
25195
+ return () => {
25196
+ try {
25197
+ clearInterval(timer);
25198
+ } catch {
25199
+ }
25200
+ };
25201
+ }
24982
25202
  var PERF_REPORT_EVERY = positiveIntEnv2("MIXDOG_TUI_PERF_EVERY", 60);
25203
+ var PERF_STALL_INTERVAL_MS = positiveIntEnv2("MIXDOG_TUI_PERF_STALL_INTERVAL_MS", 100);
25204
+ var PERF_STALL_THRESHOLD_MS = positiveIntEnv2("MIXDOG_TUI_PERF_STALL_MS", 80);
25205
+ var PERF_RENDER_GAP_MS = positiveIntEnv2("MIXDOG_TUI_PERF_RENDER_GAP_MS", 120);
25206
+ function perfLog(event, fields = {}) {
25207
+ if (!PERF_ENABLED) return;
25208
+ const elapsedMs = performance3.now() - BOOT_PROFILE_START2;
25209
+ const parts = [`[mixdog-perf] +${elapsedMs.toFixed(1)}ms`, event];
25210
+ for (const [key, value] of Object.entries(fields || {})) {
25211
+ if (value === void 0 || value === null || value === "") continue;
25212
+ parts.push(`${key}=${String(value).replace(/\s+/g, "_")}`);
25213
+ }
25214
+ try {
25215
+ process.stderr.write(`${parts.join(" ")}
25216
+ `);
25217
+ } catch {
25218
+ }
25219
+ }
25220
+ function installTuiPerfProbe() {
25221
+ if (!PERF_ENABLED) return () => {
25222
+ };
25223
+ let last = performance3.now();
25224
+ let lastCpu = process.cpuUsage();
25225
+ perfLog("probe:start", {
25226
+ intervalMs: PERF_STALL_INTERVAL_MS,
25227
+ stallMs: PERF_STALL_THRESHOLD_MS,
25228
+ renderGapMs: PERF_RENDER_GAP_MS
25229
+ });
25230
+ const timer = setInterval(() => {
25231
+ const now = performance3.now();
25232
+ const elapsed = now - last;
25233
+ const lagMs = elapsed - PERF_STALL_INTERVAL_MS;
25234
+ const cpu = process.cpuUsage(lastCpu);
25235
+ last = now;
25236
+ lastCpu = process.cpuUsage();
25237
+ if (lagMs < PERF_STALL_THRESHOLD_MS) return;
25238
+ const mem = process.memoryUsage();
25239
+ perfLog("event-loop-stall", {
25240
+ lagMs: lagMs.toFixed(1),
25241
+ elapsedMs: elapsed.toFixed(1),
25242
+ cpuUserMs: (cpu.user / 1e3).toFixed(1),
25243
+ cpuSystemMs: (cpu.system / 1e3).toFixed(1),
25244
+ rssMb: (mem.rss / 1048576).toFixed(1),
25245
+ heapMb: (mem.heapUsed / 1048576).toFixed(1)
25246
+ });
25247
+ }, PERF_STALL_INTERVAL_MS);
25248
+ timer.unref?.();
25249
+ return () => {
25250
+ try {
25251
+ clearInterval(timer);
25252
+ } catch {
25253
+ }
25254
+ };
25255
+ }
24983
25256
  function makeRenderProfiler() {
24984
25257
  if (!PERF_ENABLED) return void 0;
24985
25258
  let count = 0;
24986
25259
  let sum = 0;
24987
25260
  let max = 0;
24988
25261
  let slow = 0;
25262
+ let maxGap = 0;
25263
+ let lastFrameAt = 0;
24989
25264
  return ({ renderTime } = {}) => {
25265
+ const now = performance3.now();
24990
25266
  const ms = Number(renderTime) || 0;
25267
+ const gap = lastFrameAt ? now - lastFrameAt : 0;
25268
+ lastFrameAt = now;
24991
25269
  count += 1;
24992
25270
  sum += ms;
24993
25271
  if (ms > max) max = ms;
25272
+ if (gap > maxGap) maxGap = gap;
24994
25273
  if (ms >= 16) slow += 1;
25274
+ if (gap >= PERF_RENDER_GAP_MS) {
25275
+ perfLog("render-gap", {
25276
+ gapMs: gap.toFixed(1),
25277
+ renderMs: ms.toFixed(2)
25278
+ });
25279
+ }
24995
25280
  if (count >= PERF_REPORT_EVERY) {
24996
25281
  const avg = sum / count;
24997
- try {
24998
- process.stderr.write(
24999
- `[mixdog-perf] frames=${count} avg=${avg.toFixed(2)}ms max=${max.toFixed(2)}ms slow16+=${slow}
25000
- `
25001
- );
25002
- } catch {
25003
- }
25282
+ perfLog("render-summary", {
25283
+ frames: count,
25284
+ avgMs: avg.toFixed(2),
25285
+ maxMs: max.toFixed(2),
25286
+ maxGapMs: maxGap.toFixed(1),
25287
+ slow16: slow
25288
+ });
25004
25289
  count = 0;
25005
25290
  sum = 0;
25006
25291
  max = 0;
25007
25292
  slow = 0;
25293
+ maxGap = 0;
25008
25294
  }
25009
25295
  };
25010
25296
  }
@@ -25219,6 +25505,8 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
25219
25505
  return 1;
25220
25506
  }
25221
25507
  const restoreStderr = installTuiStderrGuard();
25508
+ const stopPerfProbe = installTuiPerfProbe();
25509
+ const stopLoopProbe = installTuiLoopProbe();
25222
25510
  const restorePrimedInput = () => drainStdin(process.stdin);
25223
25511
  let restored = false;
25224
25512
  const restoreTerminal = () => {
@@ -25250,6 +25538,8 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
25250
25538
  bootProfile2("store:ready", { ms: (performance3.now() - startedAt).toFixed(1) });
25251
25539
  } catch (error) {
25252
25540
  splash.stop();
25541
+ stopPerfProbe();
25542
+ stopLoopProbe();
25253
25543
  restoreTerminal();
25254
25544
  try {
25255
25545
  process.off("exit", restoreTerminal);
@@ -25334,6 +25624,8 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
25334
25624
  }
25335
25625
  await waitUntilExit();
25336
25626
  } finally {
25627
+ stopPerfProbe();
25628
+ stopLoopProbe();
25337
25629
  clearInterval(uiHeartbeatTimer);
25338
25630
  for (const [stream, event, handler] of stdioDeathListeners.splice(0)) {
25339
25631
  try {