mixdog 0.9.16 → 0.9.18

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 (43) hide show
  1. package/package.json +2 -1
  2. package/scripts/atomic-lock-tryonce-test.mjs +66 -0
  3. package/scripts/provider-toolcall-test.mjs +79 -2
  4. package/src/mixdog-session-runtime.mjs +12 -0
  5. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +33 -1
  6. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +9 -0
  7. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +49 -0
  8. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -0
  9. package/src/runtime/agent/orchestrator/session/context-utils.mjs +7 -0
  10. package/src/runtime/agent/orchestrator/session/loop.mjs +8 -0
  11. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +32 -18
  12. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +142 -3
  13. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +108 -30
  14. package/src/runtime/agent/orchestrator/session/store.mjs +5 -0
  15. package/src/runtime/agent/orchestrator/stall-policy.mjs +22 -12
  16. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +3 -1
  17. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +15 -15
  18. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +10 -2
  19. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +8 -2
  20. package/src/runtime/channels/backends/discord-attachments.mjs +2 -2
  21. package/src/runtime/channels/backends/discord-gateway.mjs +12 -2
  22. package/src/runtime/channels/backends/discord.mjs +97 -7
  23. package/src/runtime/channels/index.mjs +150 -23
  24. package/src/runtime/channels/lib/crash-log.mjs +21 -3
  25. package/src/runtime/channels/lib/output-forwarder.mjs +118 -7
  26. package/src/runtime/channels/lib/runtime-paths.mjs +21 -19
  27. package/src/runtime/channels/lib/voice-transcription.mjs +4 -2
  28. package/src/runtime/memory/index.mjs +37 -0
  29. package/src/runtime/memory/lib/embedding-warmup.mjs +3 -0
  30. package/src/runtime/memory/lib/ko-morph.mjs +1 -0
  31. package/src/runtime/shared/atomic-file.mjs +110 -0
  32. package/src/runtime/shared/transcript-writer.mjs +46 -4
  33. package/src/session-runtime/provider-models.mjs +47 -8
  34. package/src/standalone/channel-worker.mjs +14 -2
  35. package/src/tui/app/transcript-window.mjs +137 -6
  36. package/src/tui/app/use-transcript-window.mjs +67 -10
  37. package/src/tui/components/StatusLine.jsx +1 -1
  38. package/src/tui/dist/index.mjs +436 -93
  39. package/src/tui/engine/tui-steering-persist.mjs +66 -35
  40. package/src/tui/engine.mjs +66 -14
  41. package/src/tui/index.jsx +97 -6
  42. package/src/ui/statusline-segments.mjs +54 -36
  43. 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 {
@@ -10994,6 +11100,10 @@ function measuredTranscriptRows(item, columns, toolOutputExpanded) {
10994
11100
  return entry.rows;
10995
11101
  }
10996
11102
  var STREAMING_ROW_QUANTUM = 1;
11103
+ var streamingBottomPinned = false;
11104
+ function setStreamingBottomPinned(pinned) {
11105
+ streamingBottomPinned = !!pinned;
11106
+ }
10997
11107
  function assistantTextForStreamingRowEstimate(text) {
10998
11108
  return streamingLayoutText(text);
10999
11109
  }
@@ -11007,14 +11117,16 @@ function estimateTranscriptItemRowsCached(item, columns, toolOutputExpanded) {
11007
11117
  if (!item) return Math.max(1, Math.ceil(estimateTranscriptItemRows(item, columns, toolOutputExpanded)));
11008
11118
  if (shouldSuppressFullyFailedToolItem(item)) return 0;
11009
11119
  if (item.kind === "assistant" && item.streaming) {
11010
- const estimate = streamingEstimateRows(item, columns, toolOutputExpanded);
11011
11120
  const toolExpanded2 = toolOutputExpanded ? 1 : 0;
11012
11121
  const idEntry = streamingMeasuredRowsById.get(item.id);
11013
11122
  if (idEntry && idEntry.rows > 0 && idEntry.columns === columns && idEntry.toolExpanded === toolExpanded2) {
11014
- return Math.max(idEntry.rows, estimate);
11123
+ if (streamingBottomPinned) {
11124
+ return Math.max(idEntry.rows, streamingEstimateRows(item, columns, toolOutputExpanded));
11125
+ }
11126
+ return idEntry.rows;
11015
11127
  }
11016
11128
  if (idEntry) streamingMeasuredRowsById.delete(item.id);
11017
- return estimate;
11129
+ return streamingEstimateRows(item, columns, toolOutputExpanded);
11018
11130
  }
11019
11131
  if (item.kind === "assistant" && streamingMeasuredRowsById.has(item.id)) {
11020
11132
  streamingMeasuredRowsById.delete(item.id);
@@ -11047,6 +11159,61 @@ function buildTranscriptRowIndex(items, {
11047
11159
  }
11048
11160
  return { rows, prefixRows, totalRows: prefixRows[allItems.length] || 0 };
11049
11161
  }
11162
+ function trailingStreamingItem(allItems) {
11163
+ const last = allItems.length > 0 ? allItems[allItems.length - 1] : null;
11164
+ return last && last.kind === "assistant" && last.streaming ? last : null;
11165
+ }
11166
+ function buildTranscriptRowIndexIncremental(items, {
11167
+ columns = 80,
11168
+ toolOutputExpanded = false,
11169
+ suppressMeasuredRowHeights = false,
11170
+ measuredRowsVersion = 0,
11171
+ cacheRef = null,
11172
+ prefixSig = null
11173
+ } = {}) {
11174
+ const allItems = Array.isArray(items) ? items : [];
11175
+ const tail = trailingStreamingItem(allItems);
11176
+ const holder = cacheRef || { current: null };
11177
+ if (!tail) {
11178
+ holder.current = null;
11179
+ return buildTranscriptRowIndex(allItems, {
11180
+ columns,
11181
+ toolOutputExpanded,
11182
+ suppressMeasuredRowHeights
11183
+ });
11184
+ }
11185
+ const prefixLen = allItems.length - 1;
11186
+ const cache = holder.current;
11187
+ 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) {
11188
+ {
11189
+ const tailMeasured = suppressMeasuredRowHeights ? null : measuredTranscriptRows(tail, columns, toolOutputExpanded);
11190
+ const tailRows = tailMeasured != null ? tailMeasured : estimateTranscriptItemRowsCached(tail, columns, toolOutputExpanded);
11191
+ const rows = cache.prefixRowsArr.slice();
11192
+ rows.push(tailRows);
11193
+ const prefixRows = cache.prefixPrefixRows.slice();
11194
+ prefixRows.push(cache.prefixTotal + tailRows);
11195
+ return { rows, prefixRows, totalRows: prefixRows[allItems.length] || 0 };
11196
+ }
11197
+ }
11198
+ const full = buildTranscriptRowIndex(allItems, {
11199
+ columns,
11200
+ toolOutputExpanded,
11201
+ suppressMeasuredRowHeights
11202
+ });
11203
+ holder.current = {
11204
+ columns,
11205
+ toolExpanded: toolOutputExpanded ? 1 : 0,
11206
+ suppress: suppressMeasuredRowHeights,
11207
+ version: measuredRowsVersion,
11208
+ prefixLen,
11209
+ prefixSig,
11210
+ tailId: tail.id,
11211
+ prefixRowsArr: full.rows.slice(0, prefixLen),
11212
+ prefixPrefixRows: full.prefixRows.slice(0, prefixLen + 1),
11213
+ prefixTotal: full.prefixRows[prefixLen] || 0
11214
+ };
11215
+ return full;
11216
+ }
11050
11217
  var transcriptSigPartCache = /* @__PURE__ */ new WeakMap();
11051
11218
  function transcriptStructureSignature(items, columns, toolOutputExpanded) {
11052
11219
  const list = Array.isArray(items) ? items : [];
@@ -11880,6 +12047,7 @@ function useTranscriptWindow({
11880
12047
  }) {
11881
12048
  const transcriptTotalRowsRef = useRef8(0);
11882
12049
  const preservedScrollDeltaRef = useRef8(0);
12050
+ const incrementalRowIndexCacheRef = useRef8(null);
11883
12051
  const prevViewportGeomRef = useRef8({ contentHeight: 0, floatingPanelRows: 0 });
11884
12052
  const transcriptItemElsRef = useRef8(/* @__PURE__ */ new Map());
11885
12053
  const transcriptMeasureRefCache = useRef8(/* @__PURE__ */ new Map());
@@ -11907,26 +12075,41 @@ function useTranscriptWindow({
11907
12075
  }
11908
12076
  return fn;
11909
12077
  }, []);
11910
- const transcriptStructureSig = useMemo(
11911
- () => transcriptStructureSignature(items, frameColumns, toolOutputExpanded),
11912
- [items, frameColumns, toolOutputExpanded]
12078
+ const streamingTailItem = useMemo(() => {
12079
+ const last = (items || []).length > 0 ? items[items.length - 1] : null;
12080
+ return last && last.kind === "assistant" && last.streaming ? last : null;
12081
+ }, [items]);
12082
+ const prefixLen = streamingTailItem ? (items || []).length - 1 : (items || []).length;
12083
+ const prefixSig = useMemo(
12084
+ () => transcriptStructureSignature(
12085
+ streamingTailItem ? (items || []).slice(0, prefixLen) : items || [],
12086
+ frameColumns,
12087
+ toolOutputExpanded
12088
+ ),
12089
+ [items, streamingTailItem, prefixLen, frameColumns, toolOutputExpanded]
11913
12090
  );
12091
+ const scrolledUpRowsForPin = Math.max(0, Number(scrollTargetRef.current) || 0);
12092
+ const transcriptPinnedForStreaming = followingRef.current || scrolledUpRowsForPin <= transcriptBottomSlackRows;
12093
+ setStreamingBottomPinned(transcriptPinnedForStreaming);
12094
+ const tailSig = streamingTailItem ? `a${streamingTailItem.id}:${estimateTranscriptItemRowsCached(streamingTailItem, frameColumns, toolOutputExpanded)}` : "_";
12095
+ const transcriptStructureSig = `${prefixSig}#${tailSig}`;
11914
12096
  const transcriptStreamingActive = (items || []).some(
11915
12097
  (item) => item?.kind === "assistant" && item?.streaming
11916
12098
  );
11917
- const scrolledUpRowsForPin = Math.max(0, Number(scrollTargetRef.current) || 0);
11918
- const transcriptPinnedForStreaming = followingRef.current || scrolledUpRowsForPin <= transcriptBottomSlackRows;
11919
12099
  const scrolledUpForStreamingMeasure = scrolledUpRowsForPin > transcriptBottomSlackRows;
11920
12100
  const prevEstimateGeometry = transcriptGeomRef.current?.suppressMeasuredRowHeights === true;
11921
12101
  const hasStreamingReadingAnchor = !!transcriptAnchorRef.current || transcriptAnchorDirtyRef.current;
11922
12102
  const bottomPinnedForMeasure = transcriptPinnedForStreaming;
11923
12103
  const suppressMeasuredRowHeights = bottomPinnedForMeasure || transcriptStreamingActive && (scrolledUpForStreamingMeasure && hasStreamingReadingAnchor || scrolledUpForStreamingMeasure && prevEstimateGeometry && !transcriptPinnedForStreaming);
11924
- const transcriptRowIndex = useMemo(() => buildTranscriptRowIndex(items, {
12104
+ const transcriptRowIndex = useMemo(() => buildTranscriptRowIndexIncremental(items, {
11925
12105
  columns: frameColumns,
11926
12106
  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]);
12107
+ suppressMeasuredRowHeights,
12108
+ measuredRowsVersion,
12109
+ cacheRef: incrementalRowIndexCacheRef,
12110
+ prefixSig
12111
+ // 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
12112
+ }), [prefixSig, tailSig, measuredRowsVersion, suppressMeasuredRowHeights]);
11930
12113
  const hasReadingAnchor = !!transcriptAnchorRef.current && !transcriptAnchorDirtyRef.current;
11931
12114
  const scrolledUpRows = Math.max(0, Number(scrollTargetRef.current) || 0);
11932
12115
  const scrolledUp = scrolledUpRows > transcriptBottomSlackRows;
@@ -22149,6 +22332,13 @@ import { randomBytes as randomBytes3 } from "crypto";
22149
22332
  import { join as join7 } from "path";
22150
22333
  var PENDING_MESSAGES_FILE = "session-pending-messages.json";
22151
22334
  var PENDING_MESSAGES_MODE = 384;
22335
+ var _persistChain = Promise.resolve();
22336
+ function _serialize(task) {
22337
+ const run = _persistChain.then(task, task);
22338
+ _persistChain = run.catch(() => {
22339
+ });
22340
+ return run;
22341
+ }
22152
22342
  function pendingMessagesPath() {
22153
22343
  return join7(resolvePluginData(), PENDING_MESSAGES_FILE);
22154
22344
  }
@@ -22216,59 +22406,67 @@ function removePersistRow(q, entry) {
22216
22406
  }
22217
22407
  function appendTuiSteeringPersist(leadSessionId, entry) {
22218
22408
  const text = entryPersistText(entry);
22219
- if (!text) return;
22409
+ if (!text) return Promise.resolve();
22220
22410
  const key = tuiSteeringSessionKey(leadSessionId);
22221
- if (!key) return;
22411
+ if (!key) return Promise.resolve();
22222
22412
  if (!entry.steeringPersistId) entry.steeringPersistId = newSteeringPersistId();
22223
22413
  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) {
22414
+ return _serialize(async () => {
22236
22415
  try {
22237
- process.stderr.write(`[tui] steering-queue append failed sessionId=${leadSessionId}: ${err?.message || err}
22416
+ await updateJsonAtomic(pendingMessagesPath(), (raw) => {
22417
+ const next = normalizePendingStore(raw);
22418
+ const q = Array.isArray(next.sessions[key]) ? next.sessions[key] : [];
22419
+ q.push(record);
22420
+ next.sessions[key] = q;
22421
+ const now = Date.now();
22422
+ next.updatedAt = now;
22423
+ touchPendingSessionEntry(next, key, now);
22424
+ return next;
22425
+ }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
22426
+ } catch (err) {
22427
+ try {
22428
+ process.stderr.write(`[tui] steering-queue append failed sessionId=${leadSessionId}: ${err?.message || err}
22238
22429
  `);
22239
- } catch {
22430
+ } catch {
22431
+ }
22240
22432
  }
22241
- }
22433
+ });
22242
22434
  }
22243
22435
  function dropTuiSteeringPersist(leadSessionId, entries) {
22244
22436
  const key = tuiSteeringSessionKey(leadSessionId);
22245
- if (!key) return;
22437
+ if (!key) return Promise.resolve();
22246
22438
  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) {
22439
+ if (batch.length === 0) return Promise.resolve();
22440
+ return _serialize(async () => {
22266
22441
  try {
22267
- process.stderr.write(`[tui] steering-queue drop failed sessionId=${leadSessionId}: ${err?.message || err}
22442
+ await updateJsonAtomic(pendingMessagesPath(), (raw) => {
22443
+ const next = normalizePendingStore(raw);
22444
+ const q = Array.isArray(next.sessions[key]) ? next.sessions[key].slice() : [];
22445
+ if (q.length === 0) return void 0;
22446
+ for (const entry of batch) {
22447
+ removePersistRow(q, entry);
22448
+ }
22449
+ if (q.length === 0) {
22450
+ delete next.sessions[key];
22451
+ if (next.sessionTouchedAt) delete next.sessionTouchedAt[key];
22452
+ } else {
22453
+ next.sessions[key] = q;
22454
+ }
22455
+ next.updatedAt = Date.now();
22456
+ return next;
22457
+ }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
22458
+ } catch (err) {
22459
+ try {
22460
+ process.stderr.write(`[tui] steering-queue drop failed sessionId=${leadSessionId}: ${err?.message || err}
22268
22461
  `);
22269
- } catch {
22462
+ } catch {
22463
+ }
22270
22464
  }
22271
- }
22465
+ });
22466
+ }
22467
+ function flushTuiSteeringPersist() {
22468
+ return _persistChain.catch(() => {
22469
+ });
22272
22470
  }
22273
22471
  function drainedRowToRestore(row) {
22274
22472
  if (typeof row === "string") {
@@ -22281,27 +22479,29 @@ function drainedRowToRestore(row) {
22281
22479
  }
22282
22480
  function drainTuiSteeringPersist(leadSessionId) {
22283
22481
  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) {
22482
+ if (!key) return Promise.resolve([]);
22483
+ return _serialize(async () => {
22484
+ let drained = [];
22298
22485
  try {
22299
- process.stderr.write(`[tui] steering-queue drain failed sessionId=${leadSessionId}: ${err?.message || err}
22486
+ await updateJsonAtomic(pendingMessagesPath(), (raw) => {
22487
+ const next = normalizePendingStore(raw);
22488
+ const q = Array.isArray(next.sessions[key]) ? next.sessions[key] : [];
22489
+ drained = q.map(drainedRowToRestore).filter(Boolean);
22490
+ if (drained.length === 0) return void 0;
22491
+ delete next.sessions[key];
22492
+ if (next.sessionTouchedAt) delete next.sessionTouchedAt[key];
22493
+ next.updatedAt = Date.now();
22494
+ return next;
22495
+ }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
22496
+ } catch (err) {
22497
+ try {
22498
+ process.stderr.write(`[tui] steering-queue drain failed sessionId=${leadSessionId}: ${err?.message || err}
22300
22499
  `);
22301
- } catch {
22500
+ } catch {
22501
+ }
22302
22502
  }
22303
- }
22304
- return drained;
22503
+ return drained;
22504
+ });
22305
22505
  }
22306
22506
 
22307
22507
  // src/tui/engine.mjs
@@ -22862,13 +23062,20 @@ async function createEngineSession({
22862
23062
  seq: deferredSeqCounter++,
22863
23063
  pushed: false,
22864
23064
  timer: null,
22865
- push: () => {
23065
+ // Mark the card visible and return its spec WITHOUT emitting, so a
23066
+ // batched turn-close flush can commit many specs in one set().
23067
+ materialize: () => {
22866
23068
  card.pushed = true;
22867
- if (!card.spec) return;
23069
+ if (!card.spec) return null;
22868
23070
  card.spec.deferredDisplayReady = true;
23071
+ return card.spec;
23072
+ },
23073
+ push: () => {
23074
+ const spec = entry.materialize();
23075
+ if (!spec) return;
22869
23076
  pushingFromDeferredEntry = true;
22870
23077
  try {
22871
- pushItem(card.spec);
23078
+ pushItem(spec);
22872
23079
  } finally {
22873
23080
  pushingFromDeferredEntry = false;
22874
23081
  }
@@ -22889,13 +23096,18 @@ async function createEngineSession({
22889
23096
  seq: deferredSeqCounter++,
22890
23097
  pushed: false,
22891
23098
  timer: null,
22892
- push: () => {
23099
+ materialize: () => {
22893
23100
  aggregate.pushed = true;
22894
- if (!aggregate.pendingSpec) return;
23101
+ if (!aggregate.pendingSpec) return null;
22895
23102
  aggregate.pendingSpec.deferredDisplayReady = true;
23103
+ return aggregate.pendingSpec;
23104
+ },
23105
+ push: () => {
23106
+ const spec = entry.materialize();
23107
+ if (!spec) return;
22896
23108
  pushingFromDeferredEntry = true;
22897
23109
  try {
22898
- pushItem(aggregate.pendingSpec);
23110
+ pushItem(spec);
22899
23111
  } finally {
22900
23112
  pushingFromDeferredEntry = false;
22901
23113
  }
@@ -22919,6 +23131,35 @@ async function createEngineSession({
22919
23131
  }
22920
23132
  }
22921
23133
  };
23134
+ const collectDeferredUpTo = (entry) => {
23135
+ const specs = [];
23136
+ if (!entry) return specs;
23137
+ for (const e of deferredEntries) {
23138
+ if (e.seq > entry.seq) break;
23139
+ if (e.pushed) continue;
23140
+ e.pushed = true;
23141
+ if (e.timer) {
23142
+ clearTimeout(e.timer);
23143
+ e.timer = null;
23144
+ }
23145
+ const spec = e.materialize?.();
23146
+ if (spec) specs.push(spec);
23147
+ }
23148
+ return specs;
23149
+ };
23150
+ const appendItemsBatch = (newItems, extra = {}) => {
23151
+ if (!newItems || !newItems.length) {
23152
+ set(extra);
23153
+ return;
23154
+ }
23155
+ const base = state.items.length;
23156
+ const items = [...state.items, ...newItems];
23157
+ for (let i = 0; i < newItems.length; i++) {
23158
+ const it = newItems[i];
23159
+ if (it?.id != null) itemIndexById.set(it.id, base + i);
23160
+ }
23161
+ set({ items, ...extra });
23162
+ };
22922
23163
  const markPromptCommitted = () => {
22923
23164
  if (activePromptRestore) {
22924
23165
  if (!promptCommittedCallbackCalled && typeof activePromptRestore.onCommitted === "function") {
@@ -23421,13 +23662,14 @@ async function createEngineSession({
23421
23662
  denyAllToolApprovals(cancelled ? "turn cancelled" : "turn finished");
23422
23663
  clearWatchdog();
23423
23664
  const isStaleUnwind = leadTurnEpoch !== turnEpoch;
23665
+ let closingItems = [];
23424
23666
  if (deferredEntries.length) {
23425
23667
  const last = deferredEntries[deferredEntries.length - 1];
23426
- if (last) flushDeferredUpTo(last);
23668
+ closingItems = collectDeferredUpTo(last);
23427
23669
  clearDeferredTimers();
23428
23670
  }
23429
23671
  flushDeferredBeforeImmediatePush = null;
23430
- const producedTranscriptItem = state.items.length > itemsAtTurnStart;
23672
+ const producedTranscriptItem = state.items.length + closingItems.length > itemsAtTurnStart;
23431
23673
  const reclaimed = cancelled && activePromptRestore?.reclaimed === true;
23432
23674
  if (!isStaleUnwind) activePromptRestore = null;
23433
23675
  closeThinkingSegment();
@@ -23441,15 +23683,16 @@ async function createEngineSession({
23441
23683
  const assistantOutput = (currentAssistantText || assistantText || "").trim();
23442
23684
  const isNoOpTurn = turnFinishedNormally && !cancelled && toolCards.length === 0 && !resultContent && !assistantOutput && !producedTranscriptItem;
23443
23685
  if (isStaleUnwind) {
23686
+ if (closingItems.length) appendItemsBatch(closingItems);
23444
23687
  tuiDebug(`runTurn STALE UNWIND turn=${turnIndex} \u2014 force-released; skipping shared-state writes`);
23445
23688
  } else {
23446
23689
  if (!isNoOpTurn) {
23447
23690
  state.stats.turns = (state.stats.turns || 0) + 1;
23448
23691
  }
23449
23692
  if (!reclaimed && !isNoOpTurn) {
23450
- pushItem({ kind: "turndone", id: nextId(), elapsedMs, status: turnStatus, outputTokens: finalOutputTokens, thinkingElapsedMs, verb: pickDoneVerb(turnIndex) });
23693
+ closingItems.push({ kind: "turndone", id: nextId(), elapsedMs, status: turnStatus, outputTokens: finalOutputTokens, thinkingElapsedMs, verb: pickDoneVerb(turnIndex) });
23451
23694
  }
23452
- set({
23695
+ appendItemsBatch(closingItems, {
23453
23696
  busy: false,
23454
23697
  spinner: null,
23455
23698
  thinking: null,
@@ -23638,8 +23881,8 @@ async function createEngineSession({
23638
23881
  commitSteeringQueueEntries(batch);
23639
23882
  return out;
23640
23883
  }
23641
- function restoreLeadSteeringFromDisk() {
23642
- const rows = drainTuiSteeringPersist(leadSessionId());
23884
+ async function restoreLeadSteeringFromDisk() {
23885
+ const rows = await drainTuiSteeringPersist(leadSessionId());
23643
23886
  if (!rows.length) return;
23644
23887
  const restored = [];
23645
23888
  for (const row of rows) {
@@ -23820,7 +24063,7 @@ async function createEngineSession({
23820
24063
  syncContextStats({ allowEstimated: true });
23821
24064
  return state.stats;
23822
24065
  };
23823
- restoreLeadSteeringFromDisk();
24066
+ void restoreLeadSteeringFromDisk();
23824
24067
  return {
23825
24068
  getState: () => state,
23826
24069
  patchItem,
@@ -24743,7 +24986,7 @@ async function createEngineSession({
24743
24986
  ...routeState(),
24744
24987
  stats: { ...state.stats }
24745
24988
  });
24746
- restoreLeadSteeringFromDisk();
24989
+ void restoreLeadSteeringFromDisk();
24747
24990
  return true;
24748
24991
  } finally {
24749
24992
  set({ commandBusy: false, commandStatus: null });
@@ -24768,6 +25011,7 @@ async function createEngineSession({
24768
25011
  }
24769
25012
  unsubscribeRemoteState = null;
24770
25013
  denyAllToolApprovals("runtime closing");
25014
+ await flushTuiSteeringPersist();
24771
25015
  await runtime.close(reason, options);
24772
25016
  listeners.clear();
24773
25017
  }
@@ -24953,7 +25197,7 @@ function touchUiHeartbeat(instanceId) {
24953
25197
  if (!curRaw) return void 0;
24954
25198
  if (instanceId && curRaw.instanceId !== instanceId) return void 0;
24955
25199
  return { ...curRaw, ui_heartbeat_at: Date.now(), updatedAt: Date.now() };
24956
- }, { compact: true, fsync: false, fsyncDir: false, renameFallback: "truncate" });
25200
+ }, { compact: true, fsync: false, fsyncDir: false, renameFallback: "truncate", timeoutMs: 0 });
24957
25201
  } catch {
24958
25202
  }
24959
25203
  }
@@ -24975,36 +25219,129 @@ var EXIT_HARD_DELAY_MS = positiveIntEnv2("MIXDOG_TUI_HARD_EXIT_DELAY_MS", 500);
24975
25219
  var EXIT_HARD_ENABLED = !/^(0|false|no|off)$/i.test(String(process.env.MIXDOG_TUI_HARD_EXIT || "1"));
24976
25220
  var EXIT_DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_TUI_EXIT_DEBUG || ""));
24977
25221
  var PERF_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_TUI_PERF || ""));
25222
+ var LOOP_PROBE_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_TUI_LOOP_PROBE || ""));
25223
+ var LOOP_PROBE_INTERVAL_MS = 1e3;
25224
+ var LOOP_PROBE_DRIFT_THRESHOLD_MS = 200;
24978
25225
  function positiveIntEnv2(name, fallback) {
24979
25226
  const value = Number(process.env[name]);
24980
25227
  return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
24981
25228
  }
25229
+ function installTuiLoopProbe() {
25230
+ if (!LOOP_PROBE_ENABLED) return () => {
25231
+ };
25232
+ let last = performance3.now();
25233
+ const timer = setInterval(() => {
25234
+ const now = performance3.now();
25235
+ const drift = now - last - LOOP_PROBE_INTERVAL_MS;
25236
+ last = now;
25237
+ if (drift > LOOP_PROBE_DRIFT_THRESHOLD_MS) {
25238
+ try {
25239
+ process.stderr.write(`[mixdog-loop] stall=${drift.toFixed(0)}ms
25240
+ `);
25241
+ } catch {
25242
+ }
25243
+ }
25244
+ }, LOOP_PROBE_INTERVAL_MS);
25245
+ timer.unref?.();
25246
+ return () => {
25247
+ try {
25248
+ clearInterval(timer);
25249
+ } catch {
25250
+ }
25251
+ };
25252
+ }
24982
25253
  var PERF_REPORT_EVERY = positiveIntEnv2("MIXDOG_TUI_PERF_EVERY", 60);
25254
+ var PERF_STALL_INTERVAL_MS = positiveIntEnv2("MIXDOG_TUI_PERF_STALL_INTERVAL_MS", 100);
25255
+ var PERF_STALL_THRESHOLD_MS = positiveIntEnv2("MIXDOG_TUI_PERF_STALL_MS", 80);
25256
+ var PERF_RENDER_GAP_MS = positiveIntEnv2("MIXDOG_TUI_PERF_RENDER_GAP_MS", 120);
25257
+ function perfLog(event, fields = {}) {
25258
+ if (!PERF_ENABLED) return;
25259
+ const elapsedMs = performance3.now() - BOOT_PROFILE_START2;
25260
+ const parts = [`[mixdog-perf] +${elapsedMs.toFixed(1)}ms`, event];
25261
+ for (const [key, value] of Object.entries(fields || {})) {
25262
+ if (value === void 0 || value === null || value === "") continue;
25263
+ parts.push(`${key}=${String(value).replace(/\s+/g, "_")}`);
25264
+ }
25265
+ try {
25266
+ process.stderr.write(`${parts.join(" ")}
25267
+ `);
25268
+ } catch {
25269
+ }
25270
+ }
25271
+ function installTuiPerfProbe() {
25272
+ if (!PERF_ENABLED) return () => {
25273
+ };
25274
+ let last = performance3.now();
25275
+ let lastCpu = process.cpuUsage();
25276
+ perfLog("probe:start", {
25277
+ intervalMs: PERF_STALL_INTERVAL_MS,
25278
+ stallMs: PERF_STALL_THRESHOLD_MS,
25279
+ renderGapMs: PERF_RENDER_GAP_MS
25280
+ });
25281
+ const timer = setInterval(() => {
25282
+ const now = performance3.now();
25283
+ const elapsed = now - last;
25284
+ const lagMs = elapsed - PERF_STALL_INTERVAL_MS;
25285
+ const cpu = process.cpuUsage(lastCpu);
25286
+ last = now;
25287
+ lastCpu = process.cpuUsage();
25288
+ if (lagMs < PERF_STALL_THRESHOLD_MS) return;
25289
+ const mem = process.memoryUsage();
25290
+ perfLog("event-loop-stall", {
25291
+ lagMs: lagMs.toFixed(1),
25292
+ elapsedMs: elapsed.toFixed(1),
25293
+ cpuUserMs: (cpu.user / 1e3).toFixed(1),
25294
+ cpuSystemMs: (cpu.system / 1e3).toFixed(1),
25295
+ rssMb: (mem.rss / 1048576).toFixed(1),
25296
+ heapMb: (mem.heapUsed / 1048576).toFixed(1)
25297
+ });
25298
+ }, PERF_STALL_INTERVAL_MS);
25299
+ timer.unref?.();
25300
+ return () => {
25301
+ try {
25302
+ clearInterval(timer);
25303
+ } catch {
25304
+ }
25305
+ };
25306
+ }
24983
25307
  function makeRenderProfiler() {
24984
25308
  if (!PERF_ENABLED) return void 0;
24985
25309
  let count = 0;
24986
25310
  let sum = 0;
24987
25311
  let max = 0;
24988
25312
  let slow = 0;
25313
+ let maxGap = 0;
25314
+ let lastFrameAt = 0;
24989
25315
  return ({ renderTime } = {}) => {
25316
+ const now = performance3.now();
24990
25317
  const ms = Number(renderTime) || 0;
25318
+ const gap = lastFrameAt ? now - lastFrameAt : 0;
25319
+ lastFrameAt = now;
24991
25320
  count += 1;
24992
25321
  sum += ms;
24993
25322
  if (ms > max) max = ms;
25323
+ if (gap > maxGap) maxGap = gap;
24994
25324
  if (ms >= 16) slow += 1;
25325
+ if (gap >= PERF_RENDER_GAP_MS) {
25326
+ perfLog("render-gap", {
25327
+ gapMs: gap.toFixed(1),
25328
+ renderMs: ms.toFixed(2)
25329
+ });
25330
+ }
24995
25331
  if (count >= PERF_REPORT_EVERY) {
24996
25332
  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
- }
25333
+ perfLog("render-summary", {
25334
+ frames: count,
25335
+ avgMs: avg.toFixed(2),
25336
+ maxMs: max.toFixed(2),
25337
+ maxGapMs: maxGap.toFixed(1),
25338
+ slow16: slow
25339
+ });
25004
25340
  count = 0;
25005
25341
  sum = 0;
25006
25342
  max = 0;
25007
25343
  slow = 0;
25344
+ maxGap = 0;
25008
25345
  }
25009
25346
  };
25010
25347
  }
@@ -25219,6 +25556,8 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
25219
25556
  return 1;
25220
25557
  }
25221
25558
  const restoreStderr = installTuiStderrGuard();
25559
+ const stopPerfProbe = installTuiPerfProbe();
25560
+ const stopLoopProbe = installTuiLoopProbe();
25222
25561
  const restorePrimedInput = () => drainStdin(process.stdin);
25223
25562
  let restored = false;
25224
25563
  const restoreTerminal = () => {
@@ -25250,6 +25589,8 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
25250
25589
  bootProfile2("store:ready", { ms: (performance3.now() - startedAt).toFixed(1) });
25251
25590
  } catch (error) {
25252
25591
  splash.stop();
25592
+ stopPerfProbe();
25593
+ stopLoopProbe();
25253
25594
  restoreTerminal();
25254
25595
  try {
25255
25596
  process.off("exit", restoreTerminal);
@@ -25334,6 +25675,8 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
25334
25675
  }
25335
25676
  await waitUntilExit();
25336
25677
  } finally {
25678
+ stopPerfProbe();
25679
+ stopLoopProbe();
25337
25680
  clearInterval(uiHeartbeatTimer);
25338
25681
  for (const [stream, event, handler] of stdioDeathListeners.splice(0)) {
25339
25682
  try {