fix: stabilize uok parity and startup widgets

This commit is contained in:
Mikael Hugo 2026-05-06 00:56:55 +02:00
parent 3960e42b26
commit fec9292104
8 changed files with 317 additions and 21 deletions

View file

@ -21,6 +21,8 @@ import type {
EditorTheme,
KeyId,
MarkdownTheme,
OverlayHandle,
OverlayOptions,
SlashCommand,
} from "@singularity-forge/pi-tui";
import {
@ -1837,6 +1839,64 @@ export class InteractiveMode {
/**
* Hide the extension selector.
*/
showExtensionCustom<T>(
factory: (
tui: TUI,
theme: Theme,
keybindings: KeybindingsManager,
done: (result: T) => void,
) =>
| (Component & { dispose?(): void })
| Promise<Component & { dispose?(): void }>,
options?: {
overlay?: boolean;
overlayOptions?: OverlayOptions | (() => OverlayOptions);
onHandle?: (handle: OverlayHandle) => void;
},
): Promise<T> {
if (options?.overlay === false) {
// Non-overlay custom components are not supported (yet)
return Promise.reject(
new Error("Non-overlay custom components are not supported"),
);
}
return new Promise((resolve) => {
const done = (result: T) => {
// Hide the overlay before resolving the promise
this.ui.hideOverlay();
this.ui.setFocus(this.editor);
resolve(result);
};
void (async () => {
try {
const component = await factory(
this.ui,
theme,
this.keybindings,
done,
);
const overlayOptions =
typeof options?.overlayOptions === "function"
? options.overlayOptions()
: options?.overlayOptions;
const handle = this.ui.showOverlay(component, overlayOptions);
options?.onHandle?.(handle);
} catch (error) {
this.ui.hideOverlay();
this.ui.setFocus(this.editor);
this.showError(
`Failed to create custom UI component: ${
error instanceof Error ? error.message : String(error)
}`,
);
resolve(undefined as T); // Resolve with undefined on error
}
})();
});
}
private hideExtensionSelector(): void {
this.extensionSelector?.dispose();
this.editorContainer.clear();

View file

@ -344,6 +344,8 @@ function registerSigtermHandler(currentBasePath) {
// mismatch that occurs when auto-mode terminates via signal.
writeParityHeartbeat(currentBasePath, {
ts: new Date().toISOString(),
...(s.currentUokRunId ? { runId: s.currentUokRunId } : {}),
sessionId: s.cmdCtx?.sessionManager?.getSessionId?.(),
path: pathLabel,
flags: { ...flags },
phase: "exit",

View file

@ -92,6 +92,14 @@ function loadHealthWidgetData(basePath) {
}
// ── Widget init ────────────────────────────────────────────────────────────────
const REFRESH_INTERVAL_MS = 60_000;
function safeSetWidget(ctx, key, content, options) {
try {
ctx.ui?.setWidget?.(key, content, options);
return true;
} catch {
return false;
}
}
/**
* Initialize the always-on sf-health widget (belowEditor).
* Call once from the extension entry point after context is available.
@ -101,11 +109,16 @@ export function initHealthWidget(ctx) {
const basePath = projectRoot();
// String-array fallback — used in RPC mode (factory is a no-op there)
const initialData = loadHealthWidgetData(basePath);
ctx.ui.setWidget("sf-health", buildHealthLines(initialData), {
placement: "belowEditor",
});
if (
!safeSetWidget(ctx, "sf-health", buildHealthLines(initialData), {
placement: "belowEditor",
})
) {
return;
}
// Factory-based widget for TUI mode — replaces the string-array above
ctx.ui.setWidget(
safeSetWidget(
ctx,
"sf-health",
(_tui, _theme) => {
let data = initialData;

View file

@ -21,6 +21,14 @@ export function buildNotificationWidgetLines() {
}
// ─── Widget init ────────────────────────────────────────────────────────
const REFRESH_INTERVAL_MS = 30_000;
function safeSetWidget(ctx, key, content, options) {
try {
ctx.ui?.setWidget?.(key, content, options);
return true;
} catch {
return false;
}
}
/**
* Initialize the always-on notification widget (belowEditor).
* Call once from session_start after the notification store is initialized.
@ -28,11 +36,16 @@ const REFRESH_INTERVAL_MS = 30_000;
export function initNotificationWidget(ctx) {
if (!ctx.hasUI) return;
// String-array fallback for RPC mode
ctx.ui.setWidget("sf-notifications", buildNotificationWidgetLines(), {
placement: "belowEditor",
});
if (
!safeSetWidget(ctx, "sf-notifications", buildNotificationWidgetLines(), {
placement: "belowEditor",
})
) {
return;
}
// Factory-based widget for TUI mode
ctx.ui.setWidget(
safeSetWidget(
ctx,
"sf-notifications",
(_tui, _theme) => {
let cachedLines;

View file

@ -0,0 +1,98 @@
import assert from "node:assert/strict";
import { test } from "vitest";
import {
buildParityReport,
hasCurrentParityWarning,
UNMATCHED_RUN_STALE_MS,
} from "../uok/parity-report.js";
const NOW = Date.parse("2026-05-06T00:00:00.000Z");
test("buildParityReport_legacy_anonymous_missing_exit_is_historical_not_current", () => {
const report = buildParityReport(
[
{ ts: "2026-05-05T17:21:52.209Z", path: "uok-kernel", phase: "enter" },
{
ts: "2026-05-05T17:25:52.439Z",
path: "uok-kernel",
phase: "exit",
status: "ok",
},
{ ts: "2026-05-05T17:25:18.803Z", path: "uok-kernel", phase: "enter" },
],
"/tmp/uok-parity.jsonl",
NOW,
);
assert.equal(report.legacyMissingExitEvents, 1);
assert.equal(report.missingExitEvents, 0);
assert.deepEqual(report.criticalMismatches, []);
assert.equal(hasCurrentParityWarning(report), false);
});
test("buildParityReport_fresh_run_missing_exit_is_current_warning", () => {
const report = buildParityReport(
[
{
ts: new Date(NOW - 5_000).toISOString(),
runId: "uok-run-1",
path: "uok-kernel",
phase: "enter",
},
],
"/tmp/uok-parity.jsonl",
NOW,
);
assert.equal(report.missingExitEvents, 1);
assert.equal(report.freshUnmatchedRuns[0].runId, "uok-run-1");
assert.match(report.criticalMismatches[0], /uok-run-1/);
assert.equal(hasCurrentParityWarning(report), true);
});
test("buildParityReport_stale_run_missing_exit_is_historical_not_current", () => {
const report = buildParityReport(
[
{
ts: new Date(NOW - UNMATCHED_RUN_STALE_MS - 1_000).toISOString(),
runId: "uok-old-run",
path: "uok-kernel",
phase: "enter",
},
],
"/tmp/uok-parity.jsonl",
NOW,
);
assert.equal(report.missingExitEvents, 0);
assert.equal(report.historicalUnmatchedRuns[0].runId, "uok-old-run");
assert.deepEqual(report.criticalMismatches, []);
assert.equal(hasCurrentParityWarning(report), false);
});
test("buildParityReport_run_exit_balances_enter", () => {
const report = buildParityReport(
[
{
ts: new Date(NOW - 10_000).toISOString(),
runId: "uok-run-2",
path: "uok-kernel",
phase: "enter",
},
{
ts: new Date(NOW - 5_000).toISOString(),
runId: "uok-run-2",
path: "uok-kernel",
phase: "exit",
status: "ok",
},
],
"/tmp/uok-parity.jsonl",
NOW,
);
assert.equal(report.missingExitEvents, 0);
assert.deepEqual(report.unmatchedRuns, []);
assert.equal(hasCurrentParityWarning(report), false);
});

View file

@ -1,5 +1,9 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import {
hasCurrentParityWarning,
writeParityReport,
} from "./uok/parity-report.js";
/**
* Read the last UOK parity report from <basePath>/.sf/runtime/uok-parity-report.json
* and surface any divergences or errors via ctx.ui?.notify?.().
@ -11,14 +15,18 @@ export async function summarizeParityReport(basePath, ctx, pi) {
if (!existsSync(reportPath)) return;
let report;
try {
report = JSON.parse(readFileSync(reportPath, "utf-8"));
report = writeParityReport(basePath);
} catch {
// Malformed JSON — silently skip
return;
try {
report = JSON.parse(readFileSync(reportPath, "utf-8"));
} catch {
// Malformed JSON — silently skip
return;
}
}
const mismatches = report.criticalMismatches?.length ?? 0;
const errors = report.statuses?.error ?? 0;
if (mismatches > 0 || errors > 0) {
if (hasCurrentParityWarning(report)) {
const msg =
`UOK parity report shows ${mismatches} critical mismatch${mismatches === 1 ? "" : "es"}, ` +
`${errors} error${errors === 1 ? "" : "s"} since ${report.generatedAt}. ` +

View file

@ -1,3 +1,4 @@
import { randomUUID } from "node:crypto";
import { debugLog } from "../debug-logger.js";
import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js";
import { setAuditEnvelopeEnabled } from "./audit-toggle.js";
@ -8,7 +9,11 @@ import {
resetParityCommitBlock,
signalKernelEnter,
} from "./parity-diff-capture.js";
import { writeParityHeartbeat, writeParityReport } from "./parity-report.js";
import {
hasCurrentParityWarning,
writeParityHeartbeat,
writeParityReport,
} from "./parity-report.js";
function refreshParityReport(basePath) {
try {
@ -28,8 +33,14 @@ export async function runAutoLoopWithUok(args) {
const prefs = deps.loadEffectiveSFPreferences()?.preferences;
const flags = resolveUokFlags(prefs);
const previousReport = refreshParityReport(s.basePath);
const runId = `uok-${randomUUID()}`;
s.currentUokRunId = runId;
resetParityCommitBlock();
if (previousReport && previousReport.missingExitEvents > 0) {
if (
previousReport &&
previousReport.missingExitEvents > 0 &&
hasCurrentParityWarning(previousReport)
) {
checkAndDrainMissingExit(
previousReport.enterEvents,
previousReport.exitEvents,
@ -39,6 +50,8 @@ export async function runAutoLoopWithUok(args) {
signalKernelEnter();
writeParityHeartbeat(s.basePath, {
ts: new Date().toISOString(),
runId,
sessionId: ctx.sessionManager?.getSessionId?.(),
path: resolveKernelPathLabel(flags),
flags: { ...flags },
phase: "enter",
@ -84,6 +97,8 @@ export async function runAutoLoopWithUok(args) {
} finally {
writeParityHeartbeat(s.basePath, {
ts: new Date().toISOString(),
runId,
sessionId: ctx.sessionManager?.getSessionId?.(),
path: resolveKernelPathLabel(flags),
flags: { ...flags },
phase: "exit",
@ -91,5 +106,6 @@ export async function runAutoLoopWithUok(args) {
...(error ? { error } : {}),
});
refreshParityReport(s.basePath);
if (s.currentUokRunId === runId) s.currentUokRunId = undefined;
}
}

View file

@ -9,6 +9,8 @@ import {
import { join } from "node:path";
import { sfRoot } from "../paths.js";
export const UNMATCHED_RUN_STALE_MS = 30 * 60 * 1000;
function parityLogPath(basePath) {
return join(sfRoot(basePath), "runtime", "uok-parity.jsonl");
}
@ -27,6 +29,14 @@ function isParityDiffEvent(value) {
typeof value.plane === "string"
);
}
function timestampMs(value) {
const ms = Date.parse(String(value ?? ""));
return Number.isFinite(ms) ? ms : undefined;
}
function isFreshTimestamp(value, nowMs, staleMs) {
const ms = timestampMs(value);
return ms !== undefined && nowMs - ms <= staleMs;
}
export function parseParityEvents(raw) {
return raw
.split("\n")
@ -44,12 +54,20 @@ export function parseParityEvents(raw) {
}
});
}
export function buildParityReport(events, sourcePath) {
export function buildParityReport(
events,
sourcePath,
nowMs = Date.now(),
staleMs = UNMATCHED_RUN_STALE_MS,
) {
const paths = {};
const statuses = {};
const criticalMismatches = [];
const runs = new Map();
let enterEvents = 0;
let exitEvents = 0;
let legacyEnterEvents = 0;
let legacyExitEvents = 0;
let totalDiffs = 0;
const divergencesByPlane = {
plan: 0,
@ -78,20 +96,77 @@ export function buildParityReport(events, sourcePath) {
const heartbeat = event;
increment(paths, heartbeat.path);
increment(statuses, heartbeat.status);
if (heartbeat.phase === "enter") enterEvents += 1;
if (heartbeat.phase === "exit") exitEvents += 1;
const runId =
typeof heartbeat.runId === "string" && heartbeat.runId.trim().length > 0
? heartbeat.runId
: undefined;
if (heartbeat.phase === "enter") {
enterEvents += 1;
if (runId) {
const current = runs.get(runId) ?? {
runId,
path: heartbeat.path,
enterEvents: 0,
exitEvents: 0,
enteredAt: heartbeat.ts,
exitedAt: undefined,
};
current.enterEvents += 1;
current.path = current.path ?? heartbeat.path;
current.enteredAt = current.enteredAt ?? heartbeat.ts;
runs.set(runId, current);
} else {
legacyEnterEvents += 1;
}
}
if (heartbeat.phase === "exit") {
exitEvents += 1;
if (runId) {
const current = runs.get(runId) ?? {
runId,
path: heartbeat.path,
enterEvents: 0,
exitEvents: 0,
enteredAt: undefined,
exitedAt: heartbeat.ts,
};
current.exitEvents += 1;
current.path = current.path ?? heartbeat.path;
current.exitedAt = heartbeat.ts;
runs.set(runId, current);
} else {
legacyExitEvents += 1;
}
}
if (heartbeat.status === "error") {
criticalMismatches.push(heartbeat.error ?? "parity event reported error");
}
}
const missingExitEvents = Math.max(0, enterEvents - exitEvents);
if (missingExitEvents > 0) {
const unmatchedRuns = Array.from(runs.values()).filter(
(run) => run.enterEvents > run.exitEvents,
);
const freshUnmatchedRuns = unmatchedRuns.filter((run) =>
isFreshTimestamp(run.enteredAt, nowMs, staleMs),
);
const historicalUnmatchedRuns = unmatchedRuns.filter(
(run) => !isFreshTimestamp(run.enteredAt, nowMs, staleMs),
);
const legacyMissingExitEvents = Math.max(
0,
legacyEnterEvents - legacyExitEvents,
);
const missingExitEvents = freshUnmatchedRuns.length;
if (freshUnmatchedRuns.length > 0) {
const exampleIds = freshUnmatchedRuns
.slice(0, 3)
.map((run) => run.runId)
.join(", ");
criticalMismatches.push(
`uok enter/exit mismatch: ${enterEvents} enters / ${exitEvents} exits`,
`uok run enter/exit mismatch: ${freshUnmatchedRuns.length} run(s) missing exit${exampleIds ? ` (${exampleIds})` : ""}`,
);
}
return {
generatedAt: new Date().toISOString(),
generatedAt: new Date(nowMs).toISOString(),
sourcePath,
totalEvents: events.length,
paths,
@ -100,10 +175,21 @@ export function buildParityReport(events, sourcePath) {
enterEvents,
exitEvents,
missingExitEvents,
legacyMissingExitEvents,
unmatchedRuns,
freshUnmatchedRuns,
historicalUnmatchedRuns,
totalDiffs,
divergencesByPlane,
};
}
export function hasCurrentParityWarning(report) {
const criticalMismatches = report?.criticalMismatches ?? [];
const errors = report?.statuses?.error ?? 0;
return criticalMismatches.length > 0 || errors > 0;
}
export function writeParityReport(basePath) {
const sourcePath = parityLogPath(basePath);
const raw = existsSync(sourcePath) ? readFileSync(sourcePath, "utf-8") : "";