AGENTS.md@packages/omo-senpi/src/components/memory/kibitzer · git:20260924.0a92c0e · 2026-09-24 · sha256 949aedd5a1416191
AGENTS.md@packages/omo-senpi/src/components/memory/kibitzer git:20260924.0a92c0eA
Immutable. This exact content never changes and is served at /api/v1/blob/949aedd5a1416191.
# Kibitzer surface
This directory holds the read-only recall judge surface: the resident in-process sidecar (`index.ts` is the live composition, one sidecar per bound main session) that is fed every prompt / tool_call / tool_result of its main session, wakes only for a memory candidate it has not judged, and delivers nudges only through the `nudge` tool. Kibitzer is never an actor — it judges, and only nudges. This is the only Kibitzer engine: the one-shot judge (per-launch child, candidate-set fingerprint, launch ceiling, trailing slot, process-wide `globalThis` judge slots, per-run `recall/runs/<id>` directories and the compaction-epoch stamp on the pending file) is gone, and none of it may come back under another name - there is no dual mode, and `memory.recall.enabled=false` is the only off switch.
The sidecar **must remain read-only**. Every child is quick-pinned and in-process; the parent process owns all writes. The only file I/O this surface performs on its own behalf is under `recall/sidecars/<encoded-session>/` (the child's own session JSONL and the parent-written `wakes.ndjson` audit lines), the machine-wide wake lease and the per-session directory owner lock under the identity's `runtime/locks`, and the seven-day sweep of aged sidecar directories - never mutable memory state.
## Anatomy
| Path | Purpose |
|------|---------|
| `events.ts` | Resident sidecar event feed: `createKibitzerEventStream` turns prompt / `tool_call` / `tool_result` hook payloads plus the branch snapshot into `KibitzerEvent` values (branch cursor = `getBranch().length`, newest assistant text emitted once ahead of the hook that revealed it). Bodies are redacted (`redactKibitzerEventText`: memory-core `redactUrl` then the senpi mirror) BEFORE truncation to the caps (tool args 400 / result head 600 / assistant 1500 / prompt 4000), `eval.summary` wins over `eval.code`, the newest 20 events stay verbatim and older ones fold into a one-line digest (<= 1024 chars, keeps first/last cursor). Malformed payloads return `false` and never throw into the hook. `renderKibitzerEventBatch` emits the escaped `<digest>` / `<event>` fragment for wake envelopes. The stream and the public surface live here; the payload readers and the bounded-text helpers are the two modules below, re-exported so every importer still reads from `./events`. |
| `events-payload.ts` | Hook payloads to event fields, every reader tolerant: `promptText`, `toolCallOf` / `toolResultOf` (identifiers capped at 64 chars), `toolArgsText` (`eval.summary` over `eval.code`, compact JSON otherwise), `newestAssistant` over the branch snapshot (skips `EXCLUDED_CUSTOM_TYPES`). |
| `events-text.ts` | Bounded text for the stream: `redactKibitzerEventText` (memory-core masks, then senpi's), `truncateHead` (exact cap with a `[+N chars]` marker, never splits a surrogate pair), `escapeXml`, and the fold - `FoldState`, `fragmentOf`, `digestLine` (header with count, seq and cursor range always survives the `KIBITZER_DIGEST_MAX_CHARS` cap). |
| `sensitive-output.ts` | Verbatim mirror of senpi `core/sensitive-output` (`redactSensitiveOutput`, `redactSensitiveTokenValues`); senpi does not export it and omo-senpi imports senpi types only. `events-redaction.test.ts` pins parity against the real dist module. |
| `delivery.ts` | Delivery lifecycle: marks nudges surfaced in the ledger at accept time, holds them in memory, writes the pending file (`{ version, sessionId, writtenAt, nudges }`; no compaction epoch - a parent compaction never invalidates the sidecar, it only retracts what delivery still holds), enqueues coordinator entries, and steers on the next `tool_result` when conditions permit. Compaction and shutdown drain the held nudges, clear coordinator entries, and delete the pending file. |
| `hooks.ts` | The four hook registrations behind a `KibitzerHookSink`: `before_agent_start` (delivery `markRunning`, then the prompt capture), `tool_call` (capture), `tool_result` (capture FIRST, then the delivery steer gate) and `agent_settled` (delivery `markSettled` plus the sink's bookkeeping refresh; never a wake). Every sink call is synchronous inside the handler - the host disposes the ctx when the handler returns - and the memory-child sentinels (`SENPI_MEMORY_REFLECTION`/`SENPI_MEMORY_FACTS`) keep every capture out of worker children. |
| `judge-outcome.ts` | RunnerOutcome → turn classification (`completed` / `empty` / `failed`+{`child_failed`, `child_failed_upstream`} / `dropped`+{`cancelled`}) that `sidecar-outcome.ts` builds on. A settled `"Model returned an empty response twice"` (senpi's empty-assistant recovery settling a second silent stop) is `completed` when nudges were accepted and `empty` when none were - the sidecar may legitimately end a wake without prose, so that message is the floor under the nudge tool's `terminate` hint, not a failure (#7963). `child_failed_upstream` is derived from the SETTLED failure message (`503`, `auth_unavailable`, `overloaded`) because the child settles its own turn once its retry budget and fallback chain are exhausted - there is no event-stream fail-fast, which would abort the engine's rotation in progress. The settled outcome's `model` (the rung that actually answered after a rotation) is what the wake outcome records. `normalizeGateReason` produces the bounded, sanitized reason (`redacted` on secret-like text). |
| `sidecar-prompt.ts` | Resident prompt contract: the `<kibitzer-seed>` / `<kibitzer-wake>` / `<kibitzer-reseed>` envelope renderers. Redacts, then caps, then escapes every embedded field; orders events by ascending parent cursor; states the no-memory-write rule and the exactly five read-only tool contracts (`read`, `grep`, `session_entries`, `memory` with `search`/`read` only, `nudge`). The reseed carries rejected paths, delivered paths, the task summary and the last cursor inside a whole-envelope char bound. The matching system prompt is the persona at `packages/memory-core/src/recall/assets/kibitzer-persona.md`. This module renders the seed and wake envelopes and re-exports the two below. |
| `sidecar-prompt-blocks.ts` | The blocks every envelope shares: `KIBITZER_SIDECAR_TOOL_NAMES` and the `<contract>` (three rules, five tool contracts, `memory` with `search,read` only), the `<task>` line, `KibitzerFieldCaps` / `KIBITZER_FIELD_CAPS`, and the field primitives `field` (redact, then cap, then the caller escapes), `openTag` (attributes redacted and escaped, newlines entity-encoded), `escapeText`, `singleLine`. |
| `sidecar-prompt-reseed.ts` | `renderKibitzerReseedPrompt` and `KibitzerReseedInput`: the `<kibitzer-reseed>` envelope with rejected and delivered path lists trimmed whole-line to the `KIBITZER_RESEED_MAX_CHARS` bound, omitted counts reported. |
| `sidecar.ts` | The resident lifecycle: `createKibitzerSidecar` keeps ONE child per bound main session behind a per-session mutex with states `idle` / `turn_running` / `reseeding` / `backoff` / `disposed`. Hook events only buffer (it owns the session's event stream); `offer` runs the wake policy and routes a wake as `seeded` (lazy child creation, also after backoff or reseed), `followed_up` (idle child revived) or `steered` (running turn, steer-all). Every seed/followUp first takes one machine-wide lease from `wakeSlot` and holds it until the turn settles (a steer joins the turn under the same lease); a bounded wait that finds every slot busy buffers as `slot_busy` with nothing drained and no timer armed - the next hook retries; a lock-domain failure is a `start_failed` wake that backs off. The lease is released on every exit (completed, budget, deadline, failure, shutdown, start failure) and `shutdown` aborts a wait in flight before it queues on the mutex. The child's own subscription counts tool calls (`toolBudget`, from `memory.recall.tool_budget`, aborts as `tool_budget_exceeded`), confirms which steers reached the transcript (unread ones replay by cursor through ONE followUp at settlement), and reads provider usage (`input + cacheRead`, char/4 fallback) for the reseed at 60% of `sidecarMaxTokens` (`memory.recall.sidecar_max_tokens`). Every wake is bounded from its admission: the 90 s deadline is armed before the `startChild` / `followUp` I/O (a start that never returns is abandoned at it - lease back, wake reported as `deadline`, the late handle aborted and disposed with no turn begun) and every re-arm is clamped to `startedAt + KIBITZER_WAKE_MAX_TOTAL_MS` (300 s), so a steer storm cannot extend one wake without bound. The 90 s deadline aborts without losing accepted nudges; settlement re-validates (`validateNudges` over offered ∪ searched − surfaced), hands to `deliver`, and charges the cooldown only when something was delivered. A failed child (or a child that could not start) enters jittered backoff (1 s doubling to 5 min) keeping every buffered event; `shutdown` aborts, disposes and never recreates. Each `KibitzerWakeOutcome` carries `slotWaitMs`. Every envelope cuts its payload from the stream when it is built - the seed included, so a hook captured while `startChild` is still running opens the next batch and rides the next wake instead of being drained unread; a child that could not start carries that payload (events and candidates) into the retry. `sidecar.ts` is the composition root and public surface (`createKibitzerSidecar`, `offer`, `shutdown`, the hook capture view, every public type and constant re-exported); the machinery is the seven modules below. |
| `sidecar-contract.ts` | The public defaults (`KIBITZER_WAKE_TOOL_BUDGET` 8, `KIBITZER_WAKE_DEADLINE_MS` 90 s = the quiet period a steer re-arms, `KIBITZER_WAKE_MAX_TOTAL_MS` 300 s = the hard cap on ONE wake measured from admission, which no re-arm may pass, `KIBITZER_SIDECAR_MAX_TOKENS` 48k, `KIBITZER_RESEED_FRACTION` 0.6, `KIBITZER_REJECTED_AFTER_WAKES` 3) and types (`KibitzerSidecarOptions`, `KibitzerSidecarChildInput`, `KibitzerOfferInput` / `KibitzerOfferResult`, `KibitzerSidecar`, `KibitzerSidecarTimers`). |
| `sidecar-core.ts` | One sidecar's shared state record (`SidecarCore`: config, the event stream, the cooldown, `state`, child, active turn, wake and generation counters, backoff timer, `closing` / `admission`, pending reseed, task summary, `carry`, the live offered / surfaced / delivered sets, the current wake's accepted + budget slots) plus the per-session mutex `serialized` and `whenIdle`; the internal `Payload` / `Envelope` / `Child` / `Turn` shapes (a `Turn` carries both `startedAt`, set at admission, and the derived `totalDeadlineAt` ceiling); `RUNTIME_TIMERS` (unref'd). |
| `sidecar-envelope.ts` | `payloadOf` (a drained batch plus candidates), `merge` (carried payloads oldest first, events by seq, candidates by first sight, first digest wins, cursor span widened) and `envelopeInput` (a payload shaped for `sidecar-prompt`, task summary only when asked). |
| `sidecar-turn.ts` | One tracked turn: `newTurn` (fresh accepted/budget slots), `offerPaths`, the child subscription `observe` (tool starts/ends against the budget, user `message_end` marks a steer consumed, assistant usage summed and `input + cacheRead` kept as the context estimate, provider/model recorded), `contextEstimate` (char/4 fallback), the deadline timer (`armDeadline` always arms at `min(now + wakeDeadlineMs, turn.totalDeadlineAt)`, and takes the expiry action as an argument so a wake can be bounded before it has a turn to abort), `abortTurn` (the sidecar's own cause wins over the engine's `cancelled`), `validated` (`validateNudges` over offered ∪ searched − surfaced), `report` (the closed `KibitzerWakeOutcome`). Never changes `state`. |
| `sidecar-admission.ts` | The machine-wide lease around one turn: `admit` (bounded, abortable through `core.admission`), `releaseLease` (idempotent, handed back exactly once on whichever exit comes first), `refused` (`busy` → `slot_busy`, `aborted` → `disposed`, a lock-domain error → a `start_failed` wake that backs off). |
| `sidecar-recovery.ts` | Letting go of a child: `prepareReseed` (rejected = offered ≥ 3 wakes ago and never delivered; delivered; task line; last cursor; every other offered path forgotten), `enterBackoff` / `clearBackoff` (jittered 1 s → 5 min, the timer returns to `idle`), `disposeChild`. |
| `sidecar-wake.ts` | The transitions, all under the mutex: `seed` (lease, payload cut, seed or reseed+wake envelope, deadline armed BEFORE `startChild`; a start failure carries the payload and backs off - a `category_unavailable` / `beyond_category` refusal is reported non-diagnostically through `kibitzerConfigurationFailure` and still backs off, so a provider connecting mid-session re-resolves against the live registry on the next wake), `followUp` (lease, carried + buffered batch in one envelope, deadline armed before the revival I/O; a failure carries the payload, disposes, backs off), `steer` (joins the running turn under its lease, re-arms the clamped deadline, an unconsumed steer replays at settlement), `abandonPendingStart` (the deadline firing while the child I/O is still in flight: it cannot queue on the mutex the stuck transition holds, so it settles the turn inline - lease back, wake reported as `deadline` - and the transition disposes the handle that finally arrives, carries the payload into the retry and backs off), `settle` (lease back first, nudges validated and delivered, cooldown charged only when something was delivered, unread envelopes carried, a failed turn un-offers its candidates, then idle / reseeding / backoff / disposed - or one followUp when something was carried). |
| `wake-slot.ts` | Machine-wide admission: `createKibitzerWakeSlot({ locksDirectory, maxConcurrent })` wraps memory-core's `recall-wake` counting lease (FIFO tickets, `max_concurrent_wakes` slots, pid/start-identity stale recovery, never age-based) into `acquire(signal)` → `acquired` (lease + `waitedMs`) / `busy` (the 15 s bounded wait elapsed with every slot live) / `aborted` (the caller's signal). Polls are 200 ms delays, never a spin; nothing retries on its own; filesystem failures propagate. |
| `settings.ts` | The config boundary: `resolveKibitzerSidecarSettings(recall)` maps the agent-resolved `memory.recall` block (`category`, `tool_budget`, `sidecar_max_tokens`, `max_concurrent_wakes`, `event_caps`) onto the values the sidecar, its child starter and the wake slot consume. Defaults live in the schema, not here. |
| `wake-policy.ts` | Pure policy: `decideWake` (a wake needs at least one candidate not offered this lifetime, not in the surfaced ledger, not `system/`; freshness is judged before the cooldown), `createAcceptedNudgeCooldown` (2 accepted wakes per 10 min per main session), `backoffDelayMs` (1 s doubling to 5 min, half-to-full jitter). |
| `sidecar-model.ts` | Which model answers and what the child looks like: `resolveKibitzerSidecarModel` pins `memory.recall.category` (default `quick`) and refuses the beyond-category ladder; `buildKibitzerSidecarSpec` is the normative ChildSpec (allowlist = exactly the five names, bare envelope, turn completion, depth 1); `createKibitzerSidecarChildStarter` is the production `startChild` port (persona → runtime → spec → one in-process child) whose refusals are typed `KibitzerSidecarStartError`s carrying the category and its unconnected providers - a `beyond_category` refusal asks the chain again through `resolveCategory` because the beyond-category resolution hides why the category itself came up empty, so the notice can still name the providers a `/login` would revive; `kibitzerConfigurationFailure` classifies the `category_unavailable` / `beyond_category` refusals as a permanent configuration state, distinct from transient start failures. |
| `sidecar-outcome.ts` | How a wake ended: `classifyWakeEnd` lets a sidecar abort (`tool_budget`, `deadline`, `shutdown`) win over the engine's `cancelled`, otherwise maps through `classifyJudgeTurn`; `KibitzerWakeOutcome` is the closed-value record per wake (status, cause, model, delivered nudges, steered count, tool calls, duration, slot wait, cursor span, context estimate, provider `usage` summed over the turn's assistant messages, `diagnostic`). Only a `failed` wake that is not a category configuration refusal (an additive `configuration` on the end and the outcome) is diagnostic. |
| `observe.ts` | Durable, bounded observability and retention. `kibitzerSidecarSessionDir` (unpadded URL-safe base64 of the parent session id: injective, path-safe, `decodeKibitzerSidecarDirName` round-trips it) names `recall/sidecars/<encoded-session>/`; `createKibitzerObservability` appends one `wakes.ndjson` line per settled wake there (`kibitzerWakeRecord`: status, cause, model, cursor span, tool calls, duration, slot wait, usage, delivered paths; the reason is cut before any stack frame, masked by memory-core's and senpi's secret patterns and bounded to `GATE_REASON_MAX_CHARS`, paths and model are capped, the whole line is bounded at `KIBITZER_WAKE_RECORD_MAX_CHARS`), serialized per session, and keeps the diagnostic streak: an isolated failure is silent, the third consecutive diagnostic failure of one main session appends exactly ONE `omo-kibitzer:gate` notice (`consecutiveFailures`, additive `wake`), and a normal completion or the session's shutdown resets it. A wake carrying a category configuration refusal is neither counted into nor resets that streak; the first one of a session appends ONE `omo-kibitzer:unavailable` warning (category, unconnected providers, the /login-or-pin fix), a guard forgotten at session shutdown. `own` takes the session's directory owner lock (`locks/recall-sidecar.<encoded-session>.lock`, `kibitzerSidecarOwnerLockPath`) until `onSessionShutdown` releases it. `pruneKibitzerSidecars` removes a sidecar directory idle for `KIBITZER_SIDECAR_RETENTION_MS` (7 days) - never one this process owns, never one whose owner lock is held by a live process (a dead owner is recovered only on pid/start proof), re-checking idleness under the lock - by rename to a `.prune-*` tombstone then removal; leftover tombstones are cleared first. The sweep runs once per identity when its first sidecar comes alive and at every session shutdown. `observe.ts` is the instance and public surface (everything below is re-exported from it). |
| `observe-paths.ts` | Directory identity: `encodeKibitzerSessionId` / `decodeKibitzerSidecarDirName` (unpadded base64url, injective), `kibitzerSidecarsRoot`, `kibitzerSidecarSessionDir`, `kibitzerWakesFile`, `kibitzerSidecarOwnerLockPath` and the lock/tombstone naming constants the sweep shares. |
| `observe-record.ts` | `kibitzerWakeRecord` / `KibitzerWakeRecord` (one `wakes.ndjson` line, fields capped, the whole line bounded at `KIBITZER_WAKE_RECORD_MAX_CHARS` with paths and reason dropped and `truncated: true` when it would not fit) and `redactKibitzerWakeReason` (cut before the first stack frame, both secret vocabularies, `normalizeGateReason`). |
| `observe-prune.ts` | `pruneKibitzerSidecars` and `KIBITZER_SIDECAR_RETENTION_MS`: the seven-day sweep described above - owned names skipped first, idle checked before and again under the owner lock (`waitTimeoutMs: 0`, contention means live), claim by rename to a `.prune-*` tombstone, leftover tombstones cleared. |
| `nudge-tool.ts` | The judge's only action: `nudge` tool definition (ToolDefinition) with acceptance accounting (`maxItems` early-exit via `terminate: true` when all accepted) and path forwarding to the ledger. Hints are admitted through memory-core's `describeInvalidHint`, which is observation-only: a hint states what the stored note records ("the note records that ..."), so one that addresses the agent - second person (`you`/`your`/`yours`/`yourself`), an imperative or negated imperative OPENING the sentence (`do not`, `don't`, `never`, `always`, `make sure`, `ensure`, `verify`, `check`, `run`, `use`, `read`, `stop`, `avoid`, `remember`, `keep`, `prefer`, `skip`, `consider`), or a Korean request ending (`...하세요`, `...하십시오`, `...하지 마세요`, `...해 주세요`, `...합니다`, a trailing `...지 마(라)`) - is rejected as `addresses-agent` with the reason and the fix ("restate what the note records as a plain observation"). Mid-sentence `must`/`never` inside an observation and Korean plain-form endings (`...한다` / `...이다`) stay valid. The other rejection reasons are unchanged: a path outside the offered candidates, an already surfaced path, a `system/` path, secret-like material (named first when a hint breaks both rules), the shape budget (`empty`, `too-long`, `multiline`, `decision-commentary`) and the spent `maxItems` budget. A rejection leaves the loop open for the one correction the contract allows. |
| `notice.ts` | Entry renderers and type constants: `GATE_ENTRY_TYPE` (the persistent-failure notice; `KibitzerGateRecord` keeps the one-shot fields, `runId` included, and gains the additive `wake`), `NUDGED_ENTRY_TYPE` (delivery result trace), `UNAVAILABLE_ENTRY_TYPE` (the once-per-session configuration notice for a dead category chain - warning tone, names the category, the unconnected providers and the /login-or-pin fix), gate reason normalization (bounded, sanitized, secrets redacted). The gate renderer draws only records carrying `consecutiveFailures`, so an isolated failure never reaches the screen; every renderer is fail-closed on malformed stored records. |
| `task-runtime.ts` | Memoized task runtime loader (primed at registration, resolved per run, cached keyed by asset name). A throw is persona unavailable; import rejection is reported once as `omo-senpi memory boot asset unavailable`. |
| `tools/` | The resident sidecar's member-scoped, read-only tool closures (`index.ts` registry = exactly `read`, `grep`, `session_entries`, `memory`, `nudge`; no aliases, no builtin passthrough). `read.ts`/`grep.ts` stay inside the workspace realpath (`path-safety.ts` rejects traversal, absolute paths and symlink escapes); `grep.ts` enumerates candidates with `git ls-files -z --cached --others --exclude-standard` when the workspace root is inside a work tree (so `.gitignore` is honored; any git failure or a non-git root falls back to the symlink-free walk, and an explicitly named file is scanned as given) and bounds the scan with `caps.grepScanFiles`/`grepScanBytes`/`grepScanMs` plus the turn's AbortSignal (checked per directory while enumerating and before every file) - the first limit that trips keeps the matches found so far and adds `stopped: "files" | "bytes" | "time" | "matches" | "aborted"` next to `truncated: true`, while a run that trips nothing still returns exactly `{ matches, truncated: false }`; `session-read.ts` pages the parent-refreshed branch snapshot after a cursor, omitting `EXCLUDED_CUSTOM_TYPES` plus `omo-kibitzer:nudged`/`omo-kibitzer:gate`/`omo-kibitzer:unavailable`; `memory.ts` exposes only `search`/`read` over the committed recall corpus (HEAD only, `system/` hidden, search hits feed the nudge allowed set); `nudge.ts` re-binds the nudge closure to `offered ∪ searched` per call. Every result is redacted (`redactUrl`) before its cap (`caps.ts`) and charges the per-wake `budget.ts` (`result.ts` turns exhaustion into a structured `tool_budget_exceeded` rejection and forwards senpi's third `execute` argument, the turn AbortSignal, to the closure). Tests: `index.test.ts`, `grep.test.ts`, `session-read.test.ts`, `memory.test.ts`, `path-safety.test.ts`. |
| `index.ts` (formerly `composition.ts`) | The live composition: `createKibitzerComposition` owns ONE `KibitzerSidecar` per bound main session (created lazily on the session's first hook when the agent-resolved `memory.recall.enabled` is on - the only off switch - and never per identity), built from the identity paths (`recall/sidecars/<encoded-session>/` via `kibitzerSidecarSessionDir` = unpadded URL-safe base64 of the session id, the repo for the read-only `memory` tool, `runtime/locks` for the wake slot), `resolveKibitzerSidecarSettings` (category into the child starter, tool budget, max tokens, event caps, `max_concurrent_wakes` into `createKibitzerWakeSlot`) and the shared recall corpus cache. Its hook sink reads the ctx synchronously - branch snapshot (event cursor and the `session_entries` page), model registry, payload - captures the event into the sidecar's stream, and only then detaches the lexical collection over the plain snapshot (prompt text or the `ToolArgWindow` texts as planner hints) into `sidecar.offer`; `tool_result` only completes the rich event. Delivery is the unchanged `delivery.ts` (`deliver` = `accept` at settlement: ledger mark, hold, steer/prompt drain); every settled wake goes to `observe.ts` (the `wakes.ndjson` line and the gate streak) and the sidecar directory is owned from creation (`observe.own`) to shutdown; compaction only drains delivery; shutdown disposes the sidecar (lease released), releases the directory (`observe.onSessionShutdown`, which also sweeps aged directories) and drains delivery - `wiring.onSessionShutdown` races this whole sequence against the session drain deadline (`shutdown-drain.ts` `raceDetached`) and lets it finish detached when the budget runs out, so the host's shutdown is never blocked by it while the lease and the owner lock are still released; `whenIdle` waits for detached collections, running turns and pending wake records; `activeSessions` lists live sidecars. `childStarter` exposes the starter's QA seams (`createRunner`, `createSession`, `loadPersona`, `loadTaskRuntime`) and `onWake` is a QA seam fed after the record is written. |
| `compat.test.ts` | Kibitzer recall compat: minimal smoke test of the nudge tool's type signature. |
| `delivery.test.ts` | Delivery state machine: acceptance → ledger mark + coordinator enqueue, tool_result steering gate, prompt drain, compaction, shutdown cleanup. |
| `events.test.ts` | Event stream: cursor + assistant capture, exact caps, `eval.summary` preference, 50 ms / same-timestamp survival, 20-event buffer + digest fold and cap, drain continuity, escaped rendering, malformed-payload tolerance. |
| `events-redaction.test.ts` | Redaction ordering: 90KB eval + API key never reach storage/digest/render, redaction-before-truncation straddle proof, memory-core masks, and byte parity with senpi's `core/sensitive-output.js`. |
| `delivery-idle.test.ts` | Delivery idle steer: verifies the delivery steerer skips nudges when the agent is idle (even if not pending). |
| `hooks.test.ts` | Hook contract: every capture runs synchronously on the live ctx and before delivery, prompt-shape gating, memory-child sentinels, unbound-session capture without delivery, and warn-and-continue on a throwing sink, a rejecting delivery or a stale session resolver. |
| `index.test.ts` | Composition contract over a scripted collector, a `createRunner` seam and the REAL wake-lock domain in a temp identity: exactly one sidecar per bound session (two sessions of one identity, three hooks on one session), none when recall is disabled or the session is unbound, every event captured before the ctx is disposed (seed carries prompt/assistant/tool_call/tool_result with cursors, caps and planner hints), an accepted nudge reaching delivery with the ledger mark and steer plus the two-per-window cooldown, one `wakes.ndjson` line per settled wake in the child transcript's directory with the owner lock held while the sidecar lives, and shutdown aborting/disposing the child, releasing the machine slot and the directory lock (the cancelled wake recorded before shutdown resolves), and starting nothing for a racing or late offer. Every wait is an explicit signal with a bounded timeout. |
| `judge-outcome.test.ts` | Outcome classification: settled turn interpretation (responses, failures, timeouts), model recording, reason normalization. |
| `notice.test.ts` | Entry renderers: the gate notice draws only at the streak threshold (a dropped or below-threshold record draws nothing), names the recalled-memory candidates and the actionable settings hint, renders the resident record (`wake`, no `runId`) without a run line and a stored one-shot record with its `run <id>` line; the unavailable notice is a warning naming the category, the unconnected providers and the /login-or-pin fix (fail-closed on malformed records, bounded fields); the nudged recollection keeps the single fixed Kibitzer title whatever the stored opener or provenance. |
| `nudge-tool.test.ts` | Nudge tool: acceptance tracking, termination on maxItems, path forwarding, and the nudge-only hint contract - an English imperative, a second-person sentence and a Korean request ending are each rejected with the `addresses-agent` reason text, and the corrected observation is still accepted afterwards. |
| `sidecar.test.ts` | Lifecycle contract over a fake `ChildHandle`, a `fakeWakeSlot()` and manual timers (`sidecar.test-support.ts`): one child per session, steer vs followUp routing, no-new-candidate silence, settlement-race replay through one followUp, provider 429 backoff with preserved events, accepted-nudge survival across the deadline abort, shutdown during a turn, tool budget as `tool_budget_exceeded` with no diagnostic outcome, cooldown charged only for delivered wakes, reseed carrying delivered/rejected paths, start failure into backoff, a category configuration refusal reported non-diagnostically with the dead category named and self-healing once a chain provider connects; governance: a five-minute slot outage buffers as `slot_busy` with no child, timer or background retry and the first admitted seed carries every event (digest + newest 20), shutdown abandons a parked lease wait, a lock-domain failure is a backed-off start failure, configured `tool_budget` / `event_caps` / `sidecar_max_tokens` govern the wake, reseed fires at exactly 60% and not below, and the lease count returns to zero on every exit path; a hook captured while `startChild` is still pending is not drained unread (it rides the next steer) and a start failure's retry seed carries every event and candidate; a `startChild` that never resolves still ends the wake as `deadline` at 90 s with the lease handed back and the late handle aborted and disposed without a turn, and a steer storm inside the quiet period is clamped to `KIBITZER_WAKE_MAX_TOTAL_MS` and aborted at exactly 300 s. Every wait is an explicit signal with a bounded timeout. |
| `wake-slot.test.ts` | The adapter over the real lock domain in a temp directory: ten sessions on two slots admitted FIFO with at most two live, busy after the bounded wait with no ticket left, abort ends a parked wait, a dead owner reclaimed only on pid/start proof while a live owner (this process) and an unprovable identity are never stolen, double release reports false. |
| `wake-policy.test.ts` | Wake decision reasons and filtering, cooldown window sliding, backoff band bounds and the full 1 s → 5 min ladder under every jitter draw. |
| `sidecar-model.test.ts` | Category pinning (no beyond-category drift), the five-name allowlist and bare/turn spec, the starter's typed refusals (dead chains name the category and its unconnected providers), and the configuration-failure classification. |
| `sidecar-prompt.test.ts` | Envelope contract: attribute/text escaping, redaction before capping, per-field caps, cursor order and range, event window, reseed bound and omitted counts, the five-tool registry, and the packaged persona filename. |
| `observability.test.ts` | Delivery provenance: one accepted nudge delivered by steer, by an idle-coordinator flush and by the prompt drain records `via: "steer"` / `"wake"` / `"prompt"` on its `omo-kibitzer:nudged` entry. |
| `observe.test.ts` | Observability and retention over a temp identity: distinct session ids that differ only by base64url-significant characters get distinct, decodable directories, wake files and owner locks; a real sidecar wake (fake child with provider usage and a nudge) lands as exactly one `wakes.ndjson` record with cursors, model, tool calls, duration and summed usage; credentials, a stack frame, a local path and oversized fields are redacted and bounded with no secret pattern left in the file; three isolated failures append no gate notice while every wake is recorded; three consecutive diagnostic failures append exactly one notice (a fourth adds nothing, a normal completion or shutdown starts a fresh streak, streaks are per session); category configuration refusals append ONE `omo-kibitzer:unavailable` notice per session, never feed or reset the diagnostic streak, and are recorded non-diagnostic with the configuration named; the seven-day sweep removes only the idle unowned directory, never one owned by this process or locked by a live owner, clears a leftover tombstone, and the directory goes only after its session's shutdown. Every wait is an explicit signal with a bounded timeout. |
| `task-runtime.test.ts` | Runtime loader: asset resolution, cache hits, persona-unavailable throws, import-rejection reporting. |