mixdog 0.9.53 → 0.9.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/package.json +2 -1
  2. package/scripts/agent-model-liveness-test.mjs +68 -0
  3. package/scripts/anthropic-transport-policy-test.mjs +260 -0
  4. package/scripts/desktop-session-bridge-test.mjs +47 -0
  5. package/scripts/gemini-provider-test.mjs +396 -1
  6. package/scripts/grok-oauth-refresh-race-test.mjs +76 -1
  7. package/scripts/interrupted-turn-history-test.mjs +28 -0
  8. package/scripts/openai-oauth-ws-1006-retry-test.mjs +81 -1
  9. package/scripts/pending-completion-drop-test.mjs +160 -4
  10. package/scripts/pending-messages-lock-nonblocking-test.mjs +62 -0
  11. package/scripts/process-lifecycle-test.mjs +18 -4
  12. package/scripts/prompt-input-parity-test.mjs +145 -0
  13. package/scripts/provider-admission-scheduler-test.mjs +4 -5
  14. package/scripts/provider-contract-test.mjs +91 -33
  15. package/scripts/provider-toolcall-test.mjs +97 -17
  16. package/scripts/streaming-tail-window-test.mjs +146 -0
  17. package/scripts/tui-runtime-load-bench-entry.jsx +608 -0
  18. package/scripts/tui-runtime-load-bench.mjs +45 -0
  19. package/scripts/tui-store-frame-batch-test.mjs +99 -0
  20. package/scripts/tui-transcript-perf-test.mjs +367 -2
  21. package/scripts/write-backpressure-test.mjs +147 -0
  22. package/src/cli.mjs +5 -5
  23. package/src/lib/keychain-cjs.cjs +114 -25
  24. package/src/repl.mjs +11 -0
  25. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -25
  26. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -2
  27. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +50 -23
  28. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +15 -4
  29. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +136 -27
  30. package/src/runtime/agent/orchestrator/providers/gemini.mjs +104 -20
  31. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +96 -75
  32. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +40 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +5 -0
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +35 -4
  35. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +3 -1
  36. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +49 -9
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +14 -2
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +9 -4
  39. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +53 -13
  40. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +145 -23
  41. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +20 -4
  42. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +316 -63
  43. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +23 -1
  44. package/src/runtime/agent/orchestrator/session/store.mjs +46 -1
  45. package/src/runtime/agent/orchestrator/stall-policy.mjs +6 -13
  46. package/src/runtime/memory/lib/trace-store.mjs +66 -4
  47. package/src/runtime/shared/buffered-appender.mjs +83 -4
  48. package/src/runtime/shared/memory-snapshot.mjs +45 -36
  49. package/src/runtime/shared/process-lifecycle.mjs +166 -45
  50. package/src/runtime/shared/process-shutdown.mjs +2 -1
  51. package/src/session-runtime/model-route-api.mjs +4 -1
  52. package/src/session-runtime/native-search.mjs +4 -2
  53. package/src/session-runtime/provider-auth-api.mjs +8 -1
  54. package/src/session-runtime/provider-models.mjs +8 -3
  55. package/src/session-runtime/resource-api.mjs +2 -1
  56. package/src/session-runtime/runtime-core.mjs +32 -0
  57. package/src/session-runtime/session-turn-api.mjs +1 -0
  58. package/src/session-runtime/warmup-schedulers.mjs +5 -1
  59. package/src/standalone/agent-tool.mjs +19 -1
  60. package/src/tui/App.jsx +10 -4
  61. package/src/tui/app/text-layout.mjs +9 -1
  62. package/src/tui/app/transcript-window.mjs +146 -60
  63. package/src/tui/app/use-mouse-input.mjs +16 -8
  64. package/src/tui/app/use-transcript-scroll.mjs +71 -19
  65. package/src/tui/app/use-transcript-window.mjs +21 -10
  66. package/src/tui/components/PromptInput.jsx +13 -6
  67. package/src/tui/dist/index.mjs +1094 -232
  68. package/src/tui/engine/context-state.mjs +31 -31
  69. package/src/tui/engine/frame-batched-store.mjs +75 -0
  70. package/src/tui/engine/session-api-ext.mjs +6 -1
  71. package/src/tui/engine/session-api.mjs +9 -3
  72. package/src/tui/engine/session-flow.mjs +54 -27
  73. package/src/tui/engine/turn.mjs +44 -3
  74. package/src/tui/engine.mjs +515 -36
  75. package/src/tui/input-editing.mjs +33 -13
  76. package/src/tui/markdown/measure-rendered-rows.mjs +117 -5
  77. package/vendor/ink/build/ink.js +8 -16
@@ -246,21 +246,14 @@ export const PROVIDER_WS_FIRST_MEANINGFUL_TIMEOUT_MS = resolveTimeoutMs(
246
246
  { minMs: 10_000, maxMs: STALL_WARN_MS },
247
247
  );
248
248
 
249
- // WS-specific semantic idle window. Do NOT reuse
250
- // PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS here: that shared SSE default was raised to
251
- // ~285s to avoid false-aborting Anthropic effort-mode streams that legitimately
252
- // produce no deltas while thinking. The WS transport is different: a silent
253
- // OpenAI/xAI websocket turn can sit with only connection/metadata activity while
254
- // the TUI lead-turn watchdog is also 300s. If the provider waits ~285-300s
255
- // before surfacing the stall, the UI watchdog often wins and the user only sees
256
- // "Turn timed out after 300s" instead of a provider retry/fallback.
257
- //
258
- // Keep WS hangs bounded well below the TUI watchdog while still allowing normal
259
- // long reasoning streams that emit reasoning/text/tool deltas (those reset this
260
- // timer). Env-overridable for experiments.
249
+ // WS semantic idle uses the same default ceiling as OpenAI HTTP/SSE semantic
250
+ // idle. This is semantic progress only (not the WS inter-chunk byte timer), so
251
+ // reasoning/text/tool deltas reset it while metadata cannot. Keep the default
252
+ // at PROVIDER_MAX_BEFORE_WARN_MS (~285s), strictly below the 300s worker
253
+ // watchdog, rather than rounding it to 300s.
261
254
  export const PROVIDER_WS_SEMANTIC_IDLE_TIMEOUT_MS = resolveTimeoutMs(
262
255
  ['MIXDOG_PROVIDER_WS_SEMANTIC_IDLE_TIMEOUT_MS', 'MIXDOG_PROVIDER_WS_OUTPUT_IDLE_TIMEOUT_MS'],
263
- 120_000,
256
+ PROVIDER_MAX_BEFORE_WARN_MS,
264
257
  { minMs: 10_000, maxMs: PROVIDER_MAX_BEFORE_WARN_MS },
265
258
  );
266
259
 
@@ -658,6 +658,8 @@ const TRACE_COLS = [
658
658
  'cached_tokens', 'cache_write_tokens', 'duration_ms',
659
659
  'error_message', 'payload', 'parent_span_id', 'entry_id',
660
660
  ]
661
+ const PG_MAX_BIND_PARAMETERS = 65_535
662
+ const TRACE_INSERT_MAX_ROWS = Math.floor(PG_MAX_BIND_PARAMETERS / TRACE_COLS.length)
661
663
 
662
664
  // ---------------------------------------------------------------------------
663
665
  // Cross-request trace_events write queue (100ms / 500-row flush window)
@@ -665,6 +667,9 @@ const TRACE_COLS = [
665
667
 
666
668
  const TRACE_QUEUE_FLUSH_MS = 100
667
669
  const TRACE_QUEUE_MAX_ROWS = 500
670
+ // Twenty normal flush batches leaves ample room for ordinary DB jitter while
671
+ // bounding producer memory across repeated flush failures.
672
+ export const TRACE_QUEUE_MAX_PENDING_EVENTS = 10_000
668
673
 
669
674
  // Per-db queue state (keyed by db object identity via WeakMap).
670
675
  const _traceQueues = new WeakMap()
@@ -672,12 +677,54 @@ const _traceQueues = new WeakMap()
672
677
  function _getQueue(db) {
673
678
  let q = _traceQueues.get(db)
674
679
  if (!q) {
675
- q = { pending: [], timer: null, flushPromise: null }
680
+ q = { pending: [], timer: null, flushPromise: null, droppedEvents: 0 }
676
681
  _traceQueues.set(db, q)
677
682
  }
678
683
  return q
679
684
  }
680
685
 
686
+ function _pendingEventCount(q) {
687
+ return q.pending.reduce((n, w) => n + w.events.length, 0)
688
+ }
689
+
690
+ function _trimPendingQueue(q) {
691
+ let overflow = _pendingEventCount(q) - TRACE_QUEUE_MAX_PENDING_EVENTS
692
+ while (overflow > 0 && q.pending.length > 0) {
693
+ const oldest = q.pending[0]
694
+ if (oldest.events.length <= overflow) {
695
+ q.pending.shift()
696
+ q.droppedEvents += oldest.events.length
697
+ overflow -= oldest.events.length
698
+ } else {
699
+ oldest.events.splice(0, overflow)
700
+ q.droppedEvents += overflow
701
+ overflow = 0
702
+ }
703
+ }
704
+ }
705
+
706
+ function _dropWrittenEvents(wrappers, count) {
707
+ while (count > 0 && wrappers.length > 0) {
708
+ const wrapper = wrappers[0]
709
+ if (wrapper.events.length <= count) {
710
+ wrappers.shift()
711
+ count -= wrapper.events.length
712
+ } else {
713
+ wrapper.events.splice(0, count)
714
+ count = 0
715
+ }
716
+ }
717
+ }
718
+
719
+ /** Observable queue depth and overflow count for a trace database. */
720
+ export function getTraceQueueStats(db) {
721
+ const q = _traceQueues.get(db)
722
+ return {
723
+ pendingEvents: q ? _pendingEventCount(q) : 0,
724
+ droppedEvents: q?.droppedEvents ?? 0,
725
+ }
726
+ }
727
+
681
728
  async function _flushQueue(db, q) {
682
729
  // When a flush is already running, callers reuse its promise — but enqueues
683
730
  // that arrive during that flush schedule a fresh timer that fires here and
@@ -691,10 +738,12 @@ async function _flushQueue(db, q) {
691
738
  if (wrappers.length === 0) return { inserted: 0 }
692
739
  const MAX_RETRIES = 3
693
740
  q.flushPromise = (async () => {
741
+ let writtenEvents = 0
694
742
  try {
695
743
  const allEvents = wrappers.flatMap(w => w.events)
696
- return await _insertTraceEventsDirect(db, allEvents)
744
+ return await _insertTraceEventBatches(db, allEvents, count => { writtenEvents += count })
697
745
  } catch (err) {
746
+ _dropWrittenEvents(wrappers, writtenEvents)
698
747
  for (const w of wrappers) w.attempts += 1
699
748
  const keep = wrappers.filter(w => w.attempts < MAX_RETRIES)
700
749
  const dropped = wrappers.filter(w => w.attempts >= MAX_RETRIES)
@@ -702,6 +751,7 @@ async function _flushQueue(db, q) {
702
751
  __mixdogMemoryLog(`[trace-queue] dropped ${dropped.reduce((n, w) => n + w.events.length, 0)} events after ${MAX_RETRIES} retries: ${err?.message}\n`)
703
752
  }
704
753
  q.pending.unshift(...keep)
754
+ _trimPendingQueue(q)
705
755
  __mixdogMemoryLog(`[trace-queue] flush error: ${err?.message}\n`)
706
756
  throw err
707
757
  } finally {
@@ -737,7 +787,7 @@ async function drainTraceQueue(db) {
737
787
  try {
738
788
  if (q.timer) { clearTimeout(q.timer); q.timer = null }
739
789
  if (q.flushPromise) await q.flushPromise.catch(() => {})
740
- if (q.pending.length > 0) await _insertTraceEventsDirect(db, q.pending.splice(0).flatMap(w => w.events))
790
+ if (q.pending.length > 0) await _insertTraceEventBatches(db, q.pending.splice(0).flatMap(w => w.events))
741
791
  } catch (e) {
742
792
  __mixdogMemoryLog(`[trace-queue] drain failed: ${e?.message}\n`)
743
793
  }
@@ -813,7 +863,8 @@ export function enqueueTraceEvents(db, events) {
813
863
  q.pending.push({ events: [...events], attempts: 0 })
814
864
  // Row cap counts pending EVENTS, not wrappers — a single wrapper with
815
865
  // >TRACE_QUEUE_MAX_ROWS events would otherwise sit until the timer.
816
- const pendingEvents = q.pending.reduce((n, w) => n + w.events.length, 0)
866
+ _trimPendingQueue(q)
867
+ const pendingEvents = _pendingEventCount(q)
817
868
  if (pendingEvents >= TRACE_QUEUE_MAX_ROWS) {
818
869
  // Flush immediately when row cap reached — don't wait for timer.
819
870
  if (q.timer) { clearTimeout(q.timer); q.timer = null }
@@ -831,6 +882,17 @@ async function _insertTraceEventsDirect(db, events) {
831
882
  return insertTraceEvents(db, events)
832
883
  }
833
884
 
885
+ async function _insertTraceEventBatches(db, events, onBatchInserted) {
886
+ let inserted = 0
887
+ for (let start = 0; start < events.length; start += TRACE_INSERT_MAX_ROWS) {
888
+ const batch = events.slice(start, start + TRACE_INSERT_MAX_ROWS)
889
+ const result = await _insertTraceEventsDirect(db, batch)
890
+ inserted += result.inserted
891
+ onBatchInserted?.(batch.length)
892
+ }
893
+ return { inserted }
894
+ }
895
+
834
896
  export async function insertTraceEvents(db, events) {
835
897
  if (!Array.isArray(events) || events.length === 0) return { inserted: 0 }
836
898
 
@@ -16,14 +16,22 @@ import { appendFile } from 'node:fs/promises';
16
16
 
17
17
  const BUFFER_FLUSH_BYTES = 32 * 1024;
18
18
  const DEBOUNCE_MS = 50;
19
+ // Per path: comfortably above normal transcript/log bursts while bounding
20
+ // producer memory when a filesystem append is stalled.
21
+ export const BUFFERED_APPEND_MAX_BYTES = 8 * 1024 * 1024;
22
+ let totalDroppedBufferedBytes = 0;
23
+ const droppedBytesByPath = new Map();
19
24
 
20
- // path -> { chunks: string[], bytes: number, timer, flushing, inFlight, failCount }
25
+ // path -> { chunks: string[], bytes: number, timer, flushing, inFlight, failCount, droppedBytes }
21
26
  const queues = new Map();
22
27
 
23
28
  function getQueue(path) {
24
29
  let q = queues.get(path);
25
30
  if (!q) {
26
- q = { chunks: [], bytes: 0, timer: null, flushing: false, inFlight: null, failCount: 0 };
31
+ q = {
32
+ chunks: [], bytes: 0, timer: null, flushing: false, inFlight: null,
33
+ failCount: 0, droppedBytes: droppedBytesByPath.get(path) ?? 0, path,
34
+ };
27
35
  queues.set(path, q);
28
36
  }
29
37
  return q;
@@ -45,6 +53,63 @@ function deleteIfIdle(path, q) {
45
53
  }
46
54
  }
47
55
 
56
+ function recordDroppedBytes(q, bytes) {
57
+ q.droppedBytes += bytes;
58
+ totalDroppedBufferedBytes += bytes;
59
+ droppedBytesByPath.set(q.path, q.droppedBytes);
60
+ }
61
+
62
+ function keepNewestUtf8(text, maxBytes, totalBytes) {
63
+ // At most maxBytes UTF-16 code units are encoded (plus one code unit when
64
+ // the slice would split a surrogate pair), bounding this work even when a
65
+ // producer supplies an arbitrarily large string.
66
+ let windowStart = Math.max(0, text.length - maxBytes);
67
+ if (windowStart > 0
68
+ && /[\uDC00-\uDFFF]/.test(text[windowStart])
69
+ && /[\uD800-\uDBFF]/.test(text[windowStart - 1])) {
70
+ windowStart -= 1;
71
+ }
72
+ const bytes = Buffer.from(text.slice(windowStart), 'utf8');
73
+ let start = Math.max(0, bytes.length - maxBytes);
74
+ while (start < bytes.length && (bytes[start] & 0xC0) === 0x80) start += 1;
75
+ return {
76
+ text: bytes.subarray(start).toString('utf8'),
77
+ bytes: bytes.length - start,
78
+ droppedBytes: totalBytes - (bytes.length - start),
79
+ };
80
+ }
81
+
82
+ function capIncomingText(text) {
83
+ // A single native byteLength pass keeps queue accounting and flush timing
84
+ // exact; only oversized input uses bounded tail-window encoding below.
85
+ const bytes = Buffer.byteLength(text, 'utf8');
86
+ if (bytes <= BUFFERED_APPEND_MAX_BYTES) return { text, bytes, droppedBytes: 0 };
87
+ return keepNewestUtf8(text, BUFFERED_APPEND_MAX_BYTES, bytes);
88
+ }
89
+
90
+ function trimBufferedQueue(q) {
91
+ while (q.bytes > BUFFERED_APPEND_MAX_BYTES && q.chunks.length > 0) {
92
+ const oldest = q.chunks[0];
93
+ const oldestBytes = Buffer.byteLength(oldest, 'utf8');
94
+ const overflowBytes = q.bytes - BUFFERED_APPEND_MAX_BYTES;
95
+ if (oldestBytes <= overflowBytes) {
96
+ q.chunks.shift();
97
+ q.bytes -= oldestBytes;
98
+ recordDroppedBytes(q, oldestBytes);
99
+ continue;
100
+ }
101
+
102
+ const retained = keepNewestUtf8(oldest, oldestBytes - overflowBytes, oldestBytes);
103
+ const removedBytes = oldestBytes - retained.bytes;
104
+ q.chunks[0] = retained.text;
105
+ q.bytes -= removedBytes;
106
+ recordDroppedBytes(q, removedBytes);
107
+ // A character-boundary trim can remove up to three extra bytes rather
108
+ // than split a code point or exceed the hard byte cap.
109
+ if (q.bytes > BUFFERED_APPEND_MAX_BYTES) continue;
110
+ }
111
+ }
112
+
48
113
  function _flush(path, q) {
49
114
  if (q.flushing) return;
50
115
  if (q.chunks.length === 0) return;
@@ -69,6 +134,7 @@ function _flush(path, q) {
69
134
  if (q.failCount <= 3) {
70
135
  q.chunks.unshift(data);
71
136
  q.bytes += Buffer.byteLength(data, 'utf8');
137
+ trimBufferedQueue(q);
72
138
  }
73
139
  })
74
140
  .finally(() => {
@@ -89,8 +155,11 @@ function _flush(path, q) {
89
155
  export function appendBuffered(path, text) {
90
156
  if (!path || !text) return;
91
157
  const q = getQueue(path);
92
- q.chunks.push(text);
93
- q.bytes += Buffer.byteLength(text, 'utf8');
158
+ const incoming = capIncomingText(text);
159
+ q.chunks.push(incoming.text);
160
+ q.bytes += incoming.bytes;
161
+ if (incoming.droppedBytes > 0) recordDroppedBytes(q, incoming.droppedBytes);
162
+ trimBufferedQueue(q);
94
163
  if (q.bytes >= BUFFER_FLUSH_BYTES) {
95
164
  if (q.timer) { clearTimeout(q.timer); q.timer = null; }
96
165
  _flush(path, q);
@@ -99,6 +168,16 @@ export function appendBuffered(path, text) {
99
168
  }
100
169
  }
101
170
 
171
+ /** Observable overflow counters for buffered append queues. */
172
+ export function getBufferedAppenderStats(path) {
173
+ const q = queues.get(path);
174
+ return {
175
+ bufferedBytes: q?.bytes ?? 0,
176
+ droppedBytes: q?.droppedBytes ?? droppedBytesByPath.get(path) ?? 0,
177
+ totalDroppedBytes: totalDroppedBufferedBytes,
178
+ };
179
+ }
180
+
102
181
  /**
103
182
  * Synchronously drain any remaining buffered bytes for every known path.
104
183
  * Intended for process exit hooks only — best-effort, never throws.
@@ -1,28 +1,27 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import {
3
- closeSync,
4
- existsSync,
5
- fsyncSync,
6
- mkdirSync,
7
- openSync,
8
- renameSync,
9
- statSync,
10
- unlinkSync,
11
- writeFileSync,
12
- writeSync,
13
- } from 'node:fs';
3
+ mkdir,
4
+ open,
5
+ rename,
6
+ stat,
7
+ unlink,
8
+ writeFile,
9
+ } from 'node:fs/promises';
14
10
  import { freemem, totalmem } from 'node:os';
15
11
  import { join } from 'node:path';
16
12
  import { promisify } from 'node:util';
17
- import { withFileLockSync } from './atomic-file.mjs';
13
+ import { withFileLock } from './atomic-file.mjs';
18
14
  import { resolvePluginData } from './plugin-paths.mjs';
19
15
 
20
16
  const execFileAsync = promisify(execFile);
21
17
  export const MEMORY_PRESSURE_SNAPSHOT_MAX_BYTES = 64 * 1024;
22
18
  const SNAPSHOT_NAME = 'memory-pressure.jsonl';
23
19
  const SNAPSHOT_INTERVAL_MS = 10 * 60 * 1000;
20
+ const SNAPSHOT_INTERVAL_JITTER_MS = 60 * 1000;
21
+ const PERIODIC_SNAPSHOT_INTERVAL_MS = SNAPSHOT_INTERVAL_MS
22
+ + Math.floor(Math.random() * ((2 * SNAPSHOT_INTERVAL_JITTER_MS) + 1))
23
+ - SNAPSHOT_INTERVAL_JITTER_MS;
24
24
  const SNAPSHOT_RATE_LIMIT_MS = 60 * 1000;
25
- const SNAPSHOT_LOCK_TIMEOUT_MS = 2000;
26
25
  const SNAPSHOT_LOCK_STALE_MS = 10000;
27
26
 
28
27
  let periodicTimer = null;
@@ -59,42 +58,50 @@ function systemMemory() {
59
58
  }
60
59
  }
61
60
 
62
- function rotateSnapshot(paths) {
63
- const size = Number(statSync(paths.snapshot).size) || 0;
64
- try { unlinkSync(paths.previousSnapshot); } catch {}
61
+ async function pathExists(path) {
62
+ try {
63
+ await stat(path);
64
+ return true;
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ async function rotateSnapshot(paths) {
71
+ const size = Number((await stat(paths.snapshot)).size) || 0;
72
+ try { await unlink(paths.previousSnapshot); } catch {}
65
73
  if (size <= MEMORY_PRESSURE_SNAPSHOT_MAX_BYTES) {
66
- renameSync(paths.snapshot, paths.previousSnapshot);
74
+ await rename(paths.snapshot, paths.previousSnapshot);
67
75
  return;
68
76
  }
69
- writeFileSync(paths.previousSnapshot, '', { mode: 0o600 });
70
- unlinkSync(paths.snapshot);
77
+ await writeFile(paths.previousSnapshot, '', { mode: 0o600 });
78
+ await unlink(paths.snapshot);
71
79
  }
72
80
 
73
- function appendSnapshot(entry) {
81
+ async function appendSnapshot(entry) {
74
82
  const line = `${JSON.stringify(entry)}\n`;
75
83
  if (Buffer.byteLength(line) > MEMORY_PRESSURE_SNAPSHOT_MAX_BYTES) return false;
76
84
  const paths = snapshotPaths();
77
85
  try {
78
- mkdirSync(paths.directory, { recursive: true, mode: 0o700 });
79
- return withFileLockSync(paths.lock, () => {
80
- if (existsSync(paths.previousSnapshot)
81
- && statSync(paths.previousSnapshot).size > MEMORY_PRESSURE_SNAPSHOT_MAX_BYTES) {
82
- writeFileSync(paths.previousSnapshot, '', { mode: 0o600 });
86
+ await mkdir(paths.directory, { recursive: true, mode: 0o700 });
87
+ return await withFileLock(paths.lock, async () => {
88
+ if (await pathExists(paths.previousSnapshot)
89
+ && (await stat(paths.previousSnapshot)).size > MEMORY_PRESSURE_SNAPSHOT_MAX_BYTES) {
90
+ await writeFile(paths.previousSnapshot, '', { mode: 0o600 });
83
91
  }
84
- if (existsSync(paths.snapshot)
85
- && statSync(paths.snapshot).size + Buffer.byteLength(line) > MEMORY_PRESSURE_SNAPSHOT_MAX_BYTES) {
86
- rotateSnapshot(paths);
92
+ if (await pathExists(paths.snapshot)
93
+ && (await stat(paths.snapshot)).size + Buffer.byteLength(line) > MEMORY_PRESSURE_SNAPSHOT_MAX_BYTES) {
94
+ await rotateSnapshot(paths);
87
95
  }
88
- const fd = openSync(paths.snapshot, 'a', 0o600);
96
+ const fd = await open(paths.snapshot, 'a', 0o600);
89
97
  try {
90
- writeSync(fd, line, null, 'utf8');
91
- fsyncSync(fd);
98
+ await fd.write(line, null, 'utf8');
92
99
  } finally {
93
- closeSync(fd);
100
+ await fd.close();
94
101
  }
95
- return statSync(paths.snapshot).size <= MEMORY_PRESSURE_SNAPSHOT_MAX_BYTES;
102
+ return (await stat(paths.snapshot)).size <= MEMORY_PRESSURE_SNAPSHOT_MAX_BYTES;
96
103
  }, {
97
- timeoutMs: SNAPSHOT_LOCK_TIMEOUT_MS,
104
+ timeoutMs: 0,
98
105
  staleMs: SNAPSHOT_LOCK_STALE_MS,
99
106
  });
100
107
  } catch {
@@ -197,7 +204,7 @@ export async function captureMemoryPressureSnapshot(reason = 'memory-pressure')
197
204
  return appendSnapshot(entry);
198
205
  }
199
206
 
200
- function capturePeriodicMemorySnapshot() {
207
+ async function capturePeriodicMemorySnapshot() {
201
208
  if (!enabled()) return false;
202
209
  return appendSnapshot(baseSnapshot('periodic'));
203
210
  }
@@ -224,7 +231,9 @@ export function requestMemoryPressureSnapshot(reason) {
224
231
 
225
232
  export function armMemoryPressureSampling() {
226
233
  if (!enabled() || periodicTimer) return false;
227
- periodicTimer = setInterval(() => { capturePeriodicMemorySnapshot(); }, SNAPSHOT_INTERVAL_MS);
234
+ periodicTimer = setInterval(() => {
235
+ void capturePeriodicMemorySnapshot().catch(() => {});
236
+ }, PERIODIC_SNAPSHOT_INTERVAL_MS);
228
237
  periodicTimer.unref?.();
229
238
  return true;
230
239
  }