diff --git a/LifeOS/install/LIFEOS/PULSE/Observability/observability.ts b/LifeOS/install/LIFEOS/PULSE/Observability/observability.ts index a7cf16dba4..b5362672bb 100644 --- a/LifeOS/install/LIFEOS/PULSE/Observability/observability.ts +++ b/LifeOS/install/LIFEOS/PULSE/Observability/observability.ts @@ -143,10 +143,14 @@ function readJsonlByteTail(filePath: string, maxBytes: number): any[] { const size = statSync(filePath).size const start = Math.max(0, size - maxBytes) const fd = openSync(filePath, "r") - const buf = Buffer.alloc(size - start) - readSync(fd, buf, 0, buf.length, start) - closeSync(fd) - let text = buf.toString("utf-8") + let text: string + try { + const buf = Buffer.alloc(size - start) + readSync(fd, buf, 0, buf.length, start) + text = buf.toString("utf-8") + } finally { + closeSync(fd) + } if (start > 0) { const nl = text.indexOf("\n") text = nl === -1 ? "" : text.slice(nl + 1) @@ -281,6 +285,21 @@ function foldClimbLine(map: Map, line: string): void { map.set(ev.slug, points) } +// Key-count eviction. The per-key series are ring-buffered (200 points / 600 +// ticks), but the number of KEYS was unbounded: every slug and every session_id +// the daemon ever folded stayed resident for its lifetime. Nothing downstream +// reads a series whose newest point is older than the stalest run window +// (RUN_ACTIVITY.nativeStaleMs, 4h), so a day of retention is generous. Called +// on the fold path only — an unchanged log returns early and grows nothing. +const CACHE_RETENTION_MS = 24 * 60 * 60 * 1000 + +function evictStaleSeries(map: Map, now: number): void { + for (const [key, series] of map) { + const last = series[series.length - 1] + if (!last || now - last.ts > CACHE_RETENTION_MS) map.delete(key) + } +} + export function getClimbData(): Map { try { if (!existsSync(WORK_EVENTS_PATH)) return new Map() @@ -310,6 +329,7 @@ export function getClimbData(): Map { climbCache.offset += Buffer.byteLength(complete, "utf-8") + 1 for (const line of complete.split("\n")) foldClimbLine(climbCache.data, line) + evictStaleSeries(climbCache.data, Date.now()) return climbCache.data } catch { return climbCache.data @@ -440,6 +460,7 @@ export function getActivityData(): Map { const remainder = chunk.slice(lastNewline + 1) activityCache.offset = st.size - Buffer.byteLength(remainder, "utf-8") for (const line of chunk.slice(0, lastNewline).split("\n")) foldActivityLine(line) + evictStaleSeries(activityCache.bySession, Date.now()) return activityCache.bySession } catch { return activityCache.bySession @@ -3938,7 +3959,7 @@ function buildTeamFromUserFiles(appIds: string[]): OverviewTeam[] | null { if (principal) { team.push({ id: "T0", name: principal, role: "Principal", kind: "human", owns: [], - avatar: principal.charAt(0), note: "Sets direction. Everything here serves his TELOS.", + avatar: principal.charAt(0), note: "Sets direction. Everything here serves the principal's TELOS.", }) } const da = nameFrom(readMd(join(USER_DIR, "DIGITAL_ASSISTANT", "DA_IDENTITY.md"))) @@ -4213,7 +4234,10 @@ function handleLifeCardApi(): Response { // arm on a fresh install: the "run /interview" onboarding banner never rendered, // and Pulse presented the fabricated sample mission and goals to the new user as // if they were their own. Placeholder entries do not count as personalization. -const SAMPLE_ENTRY_RE = /^\**\s*\(sample\)/i +// \b not literal ")": the scaffold also writes "(sample — …)" variants (e.g. +// MISSION.md M2), which a literal match let through. Same fix as the sibling +// filter in LIFEOS/TOOLS/GenerateTelosSummary.ts. +const SAMPLE_ENTRY_RE = /^\**\s*\(sample\b/i function telosPersonalized(): boolean { try {