feat(tui+release): persist vitest memory guard, aggregate root CHANGELOG, lockstep all packages

TUI: vitest memory-guard threshold and on/off toggle now persist to
global settings (vitestAutoKillEnabled / vitestKillThresholdPct), so
they survive dashboard restarts. Stats panel shows the system-memory
used percentage next to used/free. Utilities panel exposes [+/-] to
adjust the threshold in 5% steps (50–99%).

Release: scripts/release.mjs auto-syncs a root CHANGELOG.md aggregated
from every packages/*/CHANGELOG.md, grouped by version with one
sub-block per package.

Versioning: all private @fusion/* packages joined the changesets fixed
group with the public cli + cli-alias and were aligned to 0.2.5, so
every release bumps every package and produces per-package CHANGELOG
entries that the aggregator picks up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-25 09:09:31 -07:00
parent 07ad203f29
commit c85dbc9bc6
16 changed files with 665 additions and 15 deletions

View File

@@ -378,6 +378,11 @@ function StatsPanel({ state, isFocused }: { state: DashboardState; isFocused: bo
<Text dimColor>used</Text>
<Text>{formatBytes(sys.systemFreeMem)}</Text>
<Text dimColor>free</Text>
{sys.systemTotalMem > 0 && (
<Text color={sysMemColor(sys.systemTotalMem - sys.systemFreeMem, sys.systemTotalMem)}>
{((sys.systemTotalMem - sys.systemFreeMem) / sys.systemTotalMem * 100).toFixed(1)}%
</Text>
)}
</StatRow>
<StatRow label="Cores">
<Text>{sys.cpuCount}</Text>
@@ -591,12 +596,14 @@ function ExpandedLog({ entry, index, total }: { entry: LogEntry; index: number;
function UtilitiesPanel({ state, isFocused }: { state: DashboardState; isFocused: boolean }) {
const autoKill = state.autoKillVitestOnPressure;
const thresholdPct = Math.round(state.vitestKillThreshold * 100);
const actions: Array<{ key: string; label: string }> = [
{ key: "r", label: "Refresh Stats" },
{ key: "c", label: "Clear Logs" },
{ key: "t", label: "Toggle Engine Pause" },
{ key: "k", label: "Kill Vitest Processes" },
{ key: "v", label: `Auto-Kill Vitest >90% Mem: ${autoKill ? "ON" : "OFF"}` },
{ key: "v", label: `Auto-Kill Vitest >${thresholdPct}% Mem: ${autoKill ? "ON" : "OFF"}` },
{ key: "+/-", label: `Adjust Threshold (${thresholdPct}%)` },
{ key: "?", label: "Help" },
];
return (
@@ -632,7 +639,8 @@ function HelpOverlay() {
["[r]", "Refresh stats (Utilities)"],
["[c]", "Clear logs (Utilities)"],
["[k]", "Kill all vitest processes (Utilities)"],
["[v]", "Toggle auto-kill vitest >90% mem (Utilities)"],
["[v]", "Toggle auto-kill vitest on memory pressure (Utilities)"],
["[+/-]", "Adjust vitest kill memory threshold (Utilities)"],
["[↑/↓/k/j]", "Navigate list / log entries"],
["[Home / G]", "First / last log entry (Logs)"],
["[Enter/Space]", "Expand log entry (Logs)"],
@@ -769,7 +777,7 @@ function StatusBar({ state, controller: _controller }: { state: DashboardState;
if (activeSection === "logs") {
hotkeys.push("↑↓ navigate", "w wrap", "f filter", "Enter expand");
} else if (activeSection === "utilities") {
hotkeys.push("r refresh", "c clear logs", "t toggle pause", "k kill vitest", "v auto-kill");
hotkeys.push("r refresh", "c clear logs", "t toggle pause", "k kill vitest", "v auto-kill", "+/- threshold");
} else {
hotkeys.push("Tab cycle panel", "1-5 jump");
}

View File

@@ -53,6 +53,9 @@ export class DashboardTUI {
// When true, sampleSystemStats() kills any running vitest processes if
// system memory usage crosses 90%. Toggled by [v] in the Utilities panel.
autoKillVitestOnPressure = true;
// System-memory ratio (0..1) at which auto-kill triggers. Adjustable from
// the Utilities panel via [+]/[-] in 5% steps. Clamped to [0.5, 0.99].
vitestKillThreshold = 0.9;
// Throttle so we don't spam kills while the sampler keeps firing during
// sustained pressure (sampler runs every 2s).
private lastAutoKillAt = 0;
@@ -117,6 +120,7 @@ export class DashboardTUI {
interactiveData: this.interactiveData,
interactiveView: this.interactiveView,
autoKillVitestOnPressure: this.autoKillVitestOnPressure,
vitestKillThreshold: this.vitestKillThreshold,
};
return this.cachedSnapshot;
}
@@ -210,12 +214,12 @@ export class DashboardTUI {
const usedRatio = (total - free) / total;
// 30s minimum gap between auto-kills — vitest restart and OS reclaim
// both take a few seconds; firing every 2s would flap.
if (usedRatio > 0.9 && now - this.lastAutoKillAt > 30_000) {
if (usedRatio > this.vitestKillThreshold && now - this.lastAutoKillAt > 30_000) {
this.lastAutoKillAt = now;
const result = this.killVitestProcesses();
if (result.killed > 0) {
this.warn(
`Auto-killed ${result.killed} vitest process${result.killed === 1 ? "" : "es"} (system memory at ${Math.round(usedRatio * 100)}%)`,
`Auto-killed ${result.killed} vitest process${result.killed === 1 ? "" : "es"} (system memory at ${Math.round(usedRatio * 100)}%, threshold ${Math.round(this.vitestKillThreshold * 100)}%)`,
"memory-guard",
);
}
@@ -257,15 +261,49 @@ export class DashboardTUI {
return { killed, pids };
}
adjustVitestKillThreshold(deltaPct: number): number {
const next = this.vitestKillThreshold + deltaPct / 100;
this.vitestKillThreshold = Math.max(0.5, Math.min(0.99, Math.round(next * 100) / 100));
this.notify();
void this.persistVitestKillSettings({ thresholdPct: Math.round(this.vitestKillThreshold * 100) });
return this.vitestKillThreshold;
}
toggleAutoKillVitest(): boolean {
this.autoKillVitestOnPressure = !this.autoKillVitestOnPressure;
if (!this.autoKillVitestOnPressure) {
this.lastAutoKillAt = 0;
}
this.notify();
void this.persistVitestKillSettings({ enabled: this.autoKillVitestOnPressure });
return this.autoKillVitestOnPressure;
}
/** Apply persisted values from global settings on startup. Does not
* trigger a write-back. */
hydrateVitestKillSettings(values: { enabled?: boolean; thresholdPct?: number }): void {
if (typeof values.enabled === "boolean") {
this.autoKillVitestOnPressure = values.enabled;
}
if (typeof values.thresholdPct === "number" && Number.isFinite(values.thresholdPct)) {
const ratio = values.thresholdPct / 100;
this.vitestKillThreshold = Math.max(0.5, Math.min(0.99, ratio));
}
this.notify();
}
private async persistVitestKillSettings(
partial: { enabled?: boolean; thresholdPct?: number },
): Promise<void> {
if (!this.callbacks?.onPersistVitestKillSettings) return;
try {
await this.callbacks.onPersistVitestKillSettings(partial);
} catch {
// Best-effort persistence — the in-memory toggle remains in effect
// even if disk write fails. The next adjust will retry.
}
}
setSettings(settings: SettingsValues): void {
this.settings = settings;
this.notify();
@@ -419,11 +457,23 @@ export class DashboardTUI {
case "v": {
const enabled = this.toggleAutoKillVitest();
this.log(
`Auto-kill vitest on memory pressure (>90%): ${enabled ? "ON" : "OFF"}`,
`Auto-kill vitest on memory pressure (>${Math.round(this.vitestKillThreshold * 100)}%): ${enabled ? "ON" : "OFF"}`,
"memory-guard",
);
break;
}
case "+":
case "=": {
const v = this.adjustVitestKillThreshold(+5);
this.log(`Vitest kill threshold: ${Math.round(v * 100)}%`, "memory-guard");
break;
}
case "-":
case "_": {
const v = this.adjustVitestKillThreshold(-5);
this.log(`Vitest kill threshold: ${Math.round(v * 100)}%`, "memory-guard");
break;
}
}
}

View File

@@ -72,6 +72,12 @@ export interface TUICallbacks {
onRefreshStats: () => Promise<void>;
onClearLogs: () => void;
onTogglePause: (paused: boolean) => Promise<SettingsValues>;
/** Persist vitest memory-guard settings to global settings so they
* survive across dashboard restarts. Optional — when undefined, the
* controller treats them as session-local. */
onPersistVitestKillSettings?: (
partial: { enabled?: boolean; thresholdPct?: number },
) => Promise<void>;
}
// Slim project shape used by interactive mode
@@ -280,6 +286,7 @@ export interface DashboardState {
interactiveData: InteractiveData | null;
interactiveView: InteractiveView;
autoKillVitestOnPressure: boolean;
vitestKillThreshold: number;
}
export const SECTION_ORDER: SectionId[] = ["system", "logs", "utilities", "stats", "settings"];
@@ -304,5 +311,6 @@ export function createInitialState(): DashboardState {
interactiveData: null,
interactiveView: "board",
autoKillVitestOnPressure: true,
vitestKillThreshold: 0.9,
};
}

View File

@@ -732,6 +732,18 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
globalPause: false,
};
},
onPersistVitestKillSettings: async (partial) => {
if (!store) return;
const patch: Record<string, unknown> = {};
if (typeof partial.enabled === "boolean") {
patch.vitestAutoKillEnabled = partial.enabled;
}
if (typeof partial.thresholdPct === "number") {
patch.vitestKillThresholdPct = partial.thresholdPct;
}
if (Object.keys(patch).length === 0) return;
await store.getGlobalSettingsStore().updateSettings(patch);
},
});
// Start the TUI
await tui.start();
@@ -1788,6 +1800,22 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
globalPause: settings.globalPause ?? false,
});
// Hydrate the TUI memory guard from persisted global settings so the
// user's previous toggle/threshold survives across dashboard restarts.
try {
const globalSettings = await store.getGlobalSettingsStore().getSettings();
tui.hydrateVitestKillSettings({
enabled: typeof globalSettings.vitestAutoKillEnabled === "boolean"
? globalSettings.vitestAutoKillEnabled
: undefined,
thresholdPct: typeof globalSettings.vitestKillThresholdPct === "number"
? globalSettings.vitestKillThresholdPct
: undefined,
});
} catch {
// Fall back to controller defaults if global settings can't be read.
}
// Populate initial stats
const tasks = await store.listTasks({ slim: true, includeArchived: false });
const counts = new Map<string, number>();