diff --git a/LifeOS/install/LIFEOS/DOCUMENTATION/Observability/ObservabilitySystem.md b/LifeOS/install/LIFEOS/DOCUMENTATION/Observability/ObservabilitySystem.md index 7467d6889d..75f22bd755 100644 --- a/LifeOS/install/LIFEOS/DOCUMENTATION/Observability/ObservabilitySystem.md +++ b/LifeOS/install/LIFEOS/DOCUMENTATION/Observability/ObservabilitySystem.md @@ -45,7 +45,7 @@ JSONL Sources (local disk) | Effort routing _(retired 2026-07-11)_ | `MEMORY/OBSERVABILITY/effort-router.jsonl` | — | `TheRouter.hook.ts` (retired/merged 2026-07-11) — mode/tier classification abolished, no successor writes this stream. The `MemoryReviewTrigger` MINIMAL-skip gate that read its tail is also gone. | | ISA rework | `MEMORY/OBSERVABILITY/isa-rework.jsonl` | — | `ISASync.hook.ts` Resume-After-Complete path (Algorithm v6.9.0, 2026-05-22). One row per auto-rewind: ts, session_id, slug, prev_phase, new_phase, prev_iteration, new_iteration, body_delta_bytes. | | Frame drift | `MEMORY/OBSERVABILITY/frame-drift.jsonl` | — | Algorithm VERIFY-phase emitter (v6.8.0). T1/T2/T3 boolean tests per ISA at VERIFY entry. | -| Reviewer runs | `MEMORY/OBSERVABILITY/reviewer-runs.jsonl` | — | `MemoryReviewer.ts` (autonomic memory). One row per reviewer execution: runId, transcript path, exchanges read, inference_duration_ms, parse_ok, dispatch_summary { total, by_type, succeeded, failed, failures }. | +| Reviewer runs | `MEMORY/OBSERVABILITY/reviewer-runs.jsonl` | — | `MemoryReviewer.ts` (autonomic memory). One row per reviewer execution: runId, transcript path, exchanges read, inference_duration_ms, parse_ok, dispatch_summary { total, by_type, succeeded, failed, failures, proposals_auto_applied, proposals_auto_apply_failed, and (7.40.4+) skipped_guard, skips }. `CortexHealth` accepts the pre-7.40.4 summary (missing skip pair = zero guard refusals) so an upgrade does not mark a successful last run CRITICAL. | | Reviewer fires | `MEMORY/OBSERVABILITY/reviewer-fires.jsonl` | — | `MemoryReviewFire.hook.ts` (Stop). Audit of when reviewer would fire if subprocess were unavailable. | | Memory writes (Tier A) | `MEMORY/OBSERVABILITY/memory-writes.jsonl` | — | `MemoryWriter.ts`. One row per set-overwrite to `_MEMORY.md` hot-layer files. Tracks evictions. | | Tier-B writes | `MEMORY/OBSERVABILITY/tier-b-writes.jsonl` | — | `MemorySystem.add()` routing. Audit row per logged-append to PROJECTS / CONTACTS / KNOWLEDGE / IDEAS (timestamp, type, bytes, path). | diff --git a/LifeOS/install/LIFEOS/TOOLS/CortexHealth.ts b/LifeOS/install/LIFEOS/TOOLS/CortexHealth.ts index 58270db6ac..44ed6c278f 100644 --- a/LifeOS/install/LIFEOS/TOOLS/CortexHealth.ts +++ b/LifeOS/install/LIFEOS/TOOLS/CortexHealth.ts @@ -43,13 +43,25 @@ function hasExactKeys(value: Record, allowed: readonly string[] function nonNegativeInteger(value: unknown): value is number { return Number.isInteger(value) && (value as number) >= 0; } function nonNegativeFinite(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value) && value >= 0; } +const DISPATCH_REQUIRED = ["total", "by_type", "succeeded", "failed", "failures", "proposals_auto_applied", "proposals_auto_apply_failed"] as const; +const DISPATCH_OPTIONAL = ["skipped_guard", "skips"] as const; +const DISPATCH_ALLOWED = [...DISPATCH_REQUIRED, ...DISPATCH_OPTIONAL] as const; + function validDispatchSummary(value: unknown): boolean { - if (!isPlainObject(value) || !hasExactKeys(value, ["total", "by_type", "succeeded", "failed", "failures", "skipped_guard", "skips", "proposals_auto_applied", "proposals_auto_apply_failed"])) return false; - if (![value.total, value.succeeded, value.failed, value.skipped_guard, value.proposals_auto_applied, value.proposals_auto_apply_failed].every(nonNegativeInteger)) return false; + if (!isPlainObject(value) || !hasExactKeys(value, DISPATCH_ALLOWED, DISPATCH_REQUIRED)) return false; + // 7.40.4 writer emits skipped_guard + skips. Pre-7.40.4 writers omit both. + // Missing pair = zero guard refusals (same fact the new writer records as 0 / []). + // One field without the other is a partial/corrupt row — reject. + const hasGuard = Object.prototype.hasOwnProperty.call(value, "skipped_guard"); + const hasSkips = Object.prototype.hasOwnProperty.call(value, "skips"); + if (hasGuard !== hasSkips) return false; + const skippedGuard = hasGuard ? value.skipped_guard : 0; + const skips = hasSkips ? value.skips : []; + if (![value.total, value.succeeded, value.failed, skippedGuard, value.proposals_auto_applied, value.proposals_auto_apply_failed].every(nonNegativeInteger)) return false; // A guard refusal (ESUSPECT_EROSION — the MemoryWriter blocking a net-drop) is a healthy // skip, not a failure: a valid run has every item either succeeded or guard-skipped, and // zero hard failures. Fail-closed strictness (failed=0, failures empty) is unchanged. - if (value.failed !== 0 || value.proposals_auto_apply_failed !== 0 || value.total !== (value.succeeded as number) + (value.skipped_guard as number) || !Array.isArray(value.failures) || value.failures.length !== 0 || !Array.isArray(value.skips)) return false; + if (value.failed !== 0 || value.proposals_auto_apply_failed !== 0 || value.total !== (value.succeeded as number) + (skippedGuard as number) || !Array.isArray(value.failures) || value.failures.length !== 0 || !Array.isArray(skips)) return false; if (!isPlainObject(value.by_type) || !Object.keys(value.by_type).every((key) => ["memory", "idea", "knowledge", "proposal"].includes(key)) || !Object.values(value.by_type).every(nonNegativeInteger)) return false; const byTypeTotal = Object.values(value.by_type).reduce((sum, count) => sum + (count as number), 0); return byTypeTotal === value.total && (value.proposals_auto_applied as number) <= ((value.by_type.proposal as number | undefined) ?? 0); @@ -210,3 +222,32 @@ export function collectCortexEvidence(options: CollectCortexOptions): CortexEvid retrieval: retrieval.malformedLines.length ? { status: "invalid", evidence: retrievalLog, malformedLines: retrieval.malformedLines } : validRetrievalRow(retrievalLast) ? { status: "ok", ts: retrievalLast.ts as string, queryHash: retrievalLast.query_hash as string, returnedCount: retrievalLast.returned_count as number, durationMs: retrievalLast.duration_ms as number, evidence: `${retrievalLog}:latest` } : retrievalLast ? { status: "invalid", evidence: `${retrievalLog}:latest` } : { status: "missing", evidence: retrievalLog }, proposals: { pending: proposals.rows.filter(row => row.status === "pending").length, evidence: proposalLog, available: proposals.exists, malformedLines: proposals.malformedLines }, observability: observabilityEvidence(obs), index: indexEvidence(memoryRoot, manifest, join(options.root, "LIFEOS/CORTEX_INDEX_POLICY.json"), nowMs) }; } + +function smokeTest(): number { + console.log("CortexHealth smoke test starting…"); + let pass = 0, fail = 0; + const check = (name: string, ok: boolean) => { + if (ok) { pass++; console.log(` ✓ ${name}`); } + else { fail++; console.error(` ✗ ${name}`); } + }; + const pre7404 = { + total: 3, by_type: { memory: 2, proposal: 1 }, succeeded: 3, failed: 0, failures: [], + proposals_auto_applied: 1, proposals_auto_apply_failed: 0, + }; + const v7404zero = { ...pre7404, skipped_guard: 0, skips: [] }; + const v7404skip = { + total: 1, by_type: { memory: 1 }, succeeded: 0, failed: 0, failures: [], + skipped_guard: 1, skips: [{ index: 0, type: "memory", reason: "ESUSPECT_EROSION: blocked" }], + proposals_auto_applied: 0, proposals_auto_apply_failed: 0, + }; + check("pre-7.40.4 summary (no skip fields) is valid", validDispatchSummary(pre7404)); + check("7.40.4 summary with zero skips is valid", validDispatchSummary(v7404zero)); + check("7.40.4 summary with a guard skip is valid", validDispatchSummary(v7404skip)); + check("hard failure is invalid", !validDispatchSummary({ ...v7404zero, failed: 1, failures: [{ index: 0, type: "memory", error: "x" }], succeeded: 2 })); + check("one skip field without the other is invalid", !validDispatchSummary({ ...pre7404, skipped_guard: 0 })); + check("unknown key is invalid", !validDispatchSummary({ ...v7404zero, extra: 1 })); + console.log(`CortexHealth smoke: ${pass} passed, ${fail} failed`); + return fail === 0 ? 0 : 1; +} + +if (import.meta.main) process.exit(smokeTest());