unsnooze 1.16.2 → 1.16.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.16.3 — 2026-08-22
4
+
5
+ - **A dashboard left open overnight no longer runs the machine out of memory.**
6
+ ([#16](https://github.com/saaranshM/unsnooze/issues/16)) `unsnooze status` in
7
+ a spare pane is exactly what it is for, and after nine or ten hours it was
8
+ dying on *"FATAL ERROR: Ineffective mark-compacts near heap limit — JavaScript
9
+ heap out of memory"*. The dashboard itself was innocent: React's development
10
+ build files a `performance.measure()` entry for every render, commit and
11
+ `setState` — the performance track it publishes for devtools — and Node
12
+ buffers user-timing entries forever with nothing to drain them. The status tab
13
+ repaints about three times a second (a one-second data tick plus the
14
+ 450ms logo animation), each repaint left roughly 40 entries behind, and a
15
+ night of that walks into Node's 2GB heap ceiling. Measured at ~12kB retained
16
+ per render, surviving a forced garbage collection. The dashboard now drains
17
+ that buffer while it is mounted; heap use went from climbing without limit to
18
+ flat. Nothing else in unsnooze records user timing, and no other command
19
+ renders long enough to have been affected.
20
+
21
+ - **Every release now creates its GitHub release automatically.** They were
22
+ written by hand afterwards, so 1.16.2 shipped to npm and to the website while
23
+ the repository's Releases tab still showed 1.16.1. The publish workflow now
24
+ creates the release from that version's CHANGELOG section, and waits for the
25
+ npm registry to actually serve the new version before rebuilding the website —
26
+ `npm publish` returns before the registry does, which is why 1.16.2's own
27
+ changelog entry was briefly invisible on unsnooze.dev.
28
+
3
29
  ## 1.16.2 — 2026-08-22
4
30
 
5
31
  - **A herdr revival now opens a tab in your project's workspace, not a new
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unsnooze",
3
- "version": "1.16.2",
3
+ "version": "1.16.3",
4
4
  "description": "Automatically resume Claude Code, Codex CLI, Grok, Qwen Code, Kimi CLI, OpenCode, and Antigravity sessions when 5-hour or weekly usage limits reset—on macOS, Linux and Windows, with or without a terminal multiplexer.",
5
5
  "keywords": [
6
6
  "claude",
@@ -62,6 +62,41 @@ export function installMouseCleanup(stdout = process.stdout) {
62
62
  };
63
63
  }
64
64
 
65
+ // React's development build records a `performance.measure()` entry for every
66
+ // render, commit and setState — the React Performance Track it publishes for
67
+ // devtools. Node buffers user-timing entries and nothing ever drains that
68
+ // buffer, so on a long-lived dashboard it IS the leak: the status tab repaints
69
+ // roughly three times a second (a 1s data tick plus the 450ms logo animation),
70
+ // each repaint leaves ~40 entries behind, and an overnight `unsnooze status`
71
+ // walked into Node's 2GB heap ceiling and aborted (#16). Measured at ~12 kB
72
+ // retained per render, surviving a forced GC.
73
+ //
74
+ // The entries cannot be capped (Node caps resource timing, not user timing) and
75
+ // switching React to its production build would mean setting NODE_ENV for the
76
+ // whole CLI before anything imports React. Draining is the narrow fix: nothing
77
+ // in unsnooze reads user timing, and the sweep lives exactly as long as the
78
+ // render does.
79
+ export const TIMING_SWEEP_MS = 5_000;
80
+
81
+ export function sweepReactTiming(perf = globalThis.performance) {
82
+ try {
83
+ perf?.clearMeasures?.();
84
+ perf?.clearMarks?.();
85
+ } catch { /* a runtime with no user timing has nothing to leak */ }
86
+ }
87
+
88
+ // Returns the stop function. The timer is unref'd: draining a buffer is never
89
+ // a reason to hold the process open.
90
+ export function startTimingSweep({ intervalMs = TIMING_SWEEP_MS, perf } = {}) {
91
+ sweepReactTiming(perf);
92
+ const timer = setInterval(() => sweepReactTiming(perf), intervalMs);
93
+ timer.unref?.();
94
+ return () => {
95
+ clearInterval(timer);
96
+ sweepReactTiming(perf);
97
+ };
98
+ }
99
+
65
100
  export function shouldUseDashboard({
66
101
  force = null,
67
102
  json = false,
@@ -87,13 +122,18 @@ export async function runDashboard({ tab = 'status' } = {}) {
87
122
  }
88
123
 
89
124
  installMouseCleanup(process.stdout);
90
- const instance = render(h(App, { initialTab: tab, mouseEnabled: getConfig('mouse') !== false }), {
91
- exitOnCtrlC: true,
92
- // Full-screen: separate buffer like vim / htop / less — original scrollback restored on quit
93
- alternateScreen: true,
94
- });
125
+ const stopTimingSweep = startTimingSweep();
126
+ try {
127
+ const instance = render(h(App, { initialTab: tab, mouseEnabled: getConfig('mouse') !== false }), {
128
+ exitOnCtrlC: true,
129
+ // Full-screen: separate buffer like vim / htop / less — original scrollback restored on quit
130
+ alternateScreen: true,
131
+ });
95
132
 
96
- await instance.waitUntilExit();
133
+ await instance.waitUntilExit();
134
+ } finally {
135
+ stopTimingSweep();
136
+ }
97
137
  // Belt and braces: normal quit also clears modes (provider already did).
98
138
  process.stdout.write(MOUSE_DISABLE_ALL);
99
139
  return 0;