git:20260924.ba0d923 to git:20260927.8473b53

1 added, 0 removed. Audit A to A.

# memory component
Kibitzer is a read-only recall judge; its only act is a nudge. The user-visible trace is identified as Kibitzer advice — a recollection notice titled with a single fixed "Kibitzer" (glyph ✦, accent tone; opener-era records may still carry an `opener` field, which is ignored) whose body reads "recalled memory: <hint>" (delivery provenance stays in the record, never on screen), distinct from `memory.nudge`, the every-N-turns write reminder in `nudge-wiring.ts`. Kibitzer runs as ONE resident in-process sidecar per bound main session (`kibitzer/`), whose durable trace is `recall/sidecars/<encoded-session>/`: the child's own session JSONL plus `wakes.ndjson`, one bounded redacted line per settled wake (`kibitzer/observe.ts`); the directory is pruned after seven idle days and never while its session is live. There are no per-run directories.
Letta-Code-style persistent agent memory for omo-senpi, backed by `@oh-my-opencode/memory-core` (harness-neutral; zero Senpi imports). Parity target: letta-code@a75f4d93e's local-capable matrix, executed per `.omo/plans/letta-memory-parity-port.md` with the research corpus at `.omo/ulw-research/20260809-224128/`.
## Memory read headlines
`read-classifier-wiring.ts` registers once beside the component's static wiring and reads the live bound identity repos from the session map. Files inside those repos receive kind `memory`, a POSIX repo-relative label, and a headline from the restored 100-entry `recall-openers.ts` pool. Lexical and real paths are supported; `.git` metadata (including symlink aliases) and symlinks escaping the repo are excluded. One picker spans the extension's identities and avoids immediate repeats; Senpi memoizes each read classification across redraws.
The optional `registerReadClassifier` API is feature-detected: older hosts log a debug skip and keep their existing read presentation. The feature activates when the pinned Senpi exposes that API. The classifier unregisters when the last bound session shuts down. This read presentation is independent of the memorian nudge notice, whose fixed `Aha!` title remains unchanged.
## Attribution
The memory architecture - the git-backed memory filesystem, the memory tool semantics, and background reflection - is inspired by [letta-code](https://github.com/letta-ai/letta-code), which is Apache-2.0 licensed (Copyright 2025, Letta authors). This component is an independent reimplementation written against the observable behavior of letta-code@a75f4d93e; no letta-code source was copied. "Letta" and "Letta Code" are trademarks of Letta, Inc., referenced only to describe origin.
## Anatomy
| Path | Purpose |
|------|---------|
| `index.ts` | Component factory: capability checks, config latch, session binding (`senpi-memory.session-binding`), fail-closed resume conflicts, supervisor refcount, shutdown cleanup. The identity is resolved from the SESSION's cwd (the host reports it per event; `pi.cwd` then `process.cwd()` are the fallbacks), never from the host process directory - one shared host serves sessions from many workspaces (#8556). |
| `identity-adoption.ts` | Which identity a (re)bind uses when the session already carries a binding entry: the record wins and a divergence is an `info` rebind, because config resolution is the suspect on a shared host. The fail-closed conflict is left for an explicitly configured `memory.agent` that names a different identity, and for a record whose memory repository cannot be reproduced under the current memory root. |
| `wiring.ts` | Registration surface: prompt handler, journal routing, tools, guard, skills scope, commands, trigger wiring, completion renderer/consumption, policy registration, status refresh. |
| `identity-runtime.ts` | Per-identity reflection assembly: reservation store (trigger engine), worker runner, lazy OS-sandbox transform. The agent directory it grants comes from the ENGINE (`ExtensionContext.agentDir`, read by `session-context-resolver.ts` and passed in by `wiring-runtime.ts`; adapter detection is the fallback), is resolved ONCE, and is pinned into the child's environment under every `AGENT_DIR_ENV_NAMES` entry - the child's cwd is the reflection worktree, so an unpinned child re-resolves a directory the sandbox never granted and dies on the credential lock (#8595). |
| `transient-identity.ts` | One-shot run routing (#7765): a headless (`hasUI === false`) or senpi-task rpc-child session whose identity owns no `repo/` gets `<memory>/transient-runs/<token>/agents/<id>` instead of a durable `agents/<id>` directory; `session_shutdown` removes that run root unless it grew a `repo/`, in which case it is left for the sweep to promote. `repo/` is the only durable/transient discriminator. |
| `transient-sweep.ts` | Registration-time reclaim (fire-and-forget from `index.ts`): `transient-runs/<token>` older than 24h whose owner pid is gone is removed, or promoted into `agents/<id>` when it holds a `repo/` (an existing target is never overwritten: stranded + warn); repo-less `agents/<id>` older than 7 days is removed. Anything owning a `repo/` is exempt in both areas; the sweep creates nothing. |
| `transient-age.ts` | The sweep's bounded age probe (depth 6, 512 entries). The verdict needs ONE mtime newer than the cutoff, never the newest one, so the walk stops at the first entry that proves the tree active - starting with the root's own mtime, which on a real agents root settles almost every identity without a single listing (#8412). The filesystem is a parameter so the walk's cost, not only its verdict, is testable. |
| `reflection-run-id.ts` | Mints reflection run ids one past the highest id still on disk (completion records, run/session dirs, epoch-prefixed worktrees, live reservation state), mirroring the facts lane's attempt sequence. A bare per-process counter restarted at 1 each launch and re-minted retired ids, whose durable completion records then collided (`Reflection completion record mismatch`) and wedged launch, reconcile, and finalization permanently. The reservation store awaits this factory INSIDE the scheduler lock so concurrent reservations cannot double-mint. |
| `prompt.ts` | Per-run compiled-memory injection via `before_agent_start`; composes the incoming prompt, sentinel-delimited block, (template, pinned revision) cache. The block and its pressure line are compiled at the session's pinned revision from `projection-pin.ts`, never at HEAD, so a memory commit mid-session cannot change the system prompt (#8470). The late `<memory_notice>` message carries session-volatile lines only and is omitted entirely when it would have none: its context line counts the message entries the branch's LATEST compaction pushed out of the live context (the ones before that compaction's `firstKeptEntryId`, or before the compaction entry when that id is no longer on the branch), never `getBranch().length` - a session that never compacted says nothing about its context. That recalled memory arrives on its own and has no tool to call is a standing fact, so it lives in memory-core's compiled REMINDER instead of the notice. |
| `projection-pin.ts` | Session pin for the projection (#8470): the first turn records `omo-memory:projection-pin` `{ sessionId, revision, noticedThrough, compactionId, pinnedAtMs }` and later turns reuse `revision`, so resume, host restart and reload reproduce the same bytes. Projected changes after the pin (`system/*.md` added/updated/removed, other non-skill paths added/removed) become ONE `<memory_notice>` line, each change announced once via `noticedThrough`; commits whose `Omo-Session` trailer is this session are left out (the tool result already showed them). Repins happen only where the cache is already cold or refresh was asked for: a new latest compaction, `/recompile` (`requestRefresh`), or a pinned commit that no longer resolves. A fork's copied record names the parent session and is ignored, so a fork pins fresh. |
| `tools.ts` | `memory` + `memory_apply_patch` ToolDefinitions over the core engines under the `memory-write` cross-process lock; execute-time activation gating. |
| `journal-wiring.ts` | `agent_settled` branch-delta scan + `session_start` crash reconcile into per-session transcript journals (v3_assistant_steps cursor). |
| `trigger-wiring.ts` | Trigger evaluation on successful settle only; compaction flag consumed once; manual entrypoint for `/reflect`. |
| `worker/` | Detached `senpi -p` reflection/dream child execution: run supervisor (absolute hard deadline, process-group kill; win32 `taskkill /T /F`), durable run ledger + sentinels, crash reconciliation, completion records/delivery, model resolution ladder, READ-ONLY health. See `worker/AGENTS.md`. |
| `nudge-wiring.ts` | Save-nudge accounting: counts accepted user turns since the last memory write (commit-trailer provenance), surfaces a nudge line in the compiled metadata block at `nudge.every_user_turns`. |
| `recall-wiring.ts` / `recall-session-read.ts` / `recall-query-planner-tools.ts` | Candidate side of Kibitzer recall. `recall-session-read` reads the live ctx synchronously into a plain `RecallSessionSnapshot` (the host disposes the ctx when the handler returns), builds the planner's USER-only text window and the dual-role transcript, and excludes memory-owned hidden channels (`omo-kibitzer:recall`, the memory notice) so a previous hint can't re-enter the query. `recall-wiring` runs the lexical planner over that window plus the tool-argument texts and returns `CollectedRecallCandidates` (candidates, already-surfaced set, transcript); harvested eval keys include `code` and `summary`, whole arguments are filtered per token without a 120-character cap, path words precede the command name, and tool texts get their own planner slots via `planner.ts` `toolTexts`. `collectCandidatesFromSnapshot` is the ctx-free entry the Kibitzer composition calls detached from its prompt / `tool_call` hooks; its candidates are what `sidecar.offer` judges for freshness. `recall-query-planner-tools` turns a `tool_call` payload into planner tokens, and `ToolArgWindow` keeps the last 8 pushes per session. |
| `kibitzer/delivery.ts` / `recall-drain.ts` | Delivery side. When a sidecar wake settles with validated nudges, `kibitzer/delivery.accept` marks the paths surfaced in the ledger immediately (accept time, not delivery time, so a later wake can't re-nudge them), holds the nudges in memory, writes the pending file (`{ version, sessionId, writtenAt, nudges }` - no epoch, no stamp), and enqueues each as a PASSIVE `kibitzer` entry on the `IdleInjectionCoordinator` (rides another source's flush, never causes one). At the next `tool_result` where `hasPendingMessages()` is false and the agent isn't idle, it steers ONE hidden `omo-kibitzer:recall` message with everything held and appends a `nudged` entry with `via: "steer"`; nudges salvaged from a deadline-aborted wake are still a normal `omo-kibitzer:nudged` entry, with no new entry type. `recall-drain` handles `before_agent_start`: it takes the pending file and unions it with the in-memory queue (`drainQueued`), dedupes by path, injects one recall message, marks the ledger, and appends the trace with `via: "prompt"`; memory worker children (reflection/facts sentinels) never receive it. An accepted compaction drops held nudges, coordinator entries, and the pending file through `delete` - the sidecar itself keeps its context across the parent's compaction. `recall-drain` also registers the `omo-kibitzer:*` entry renderers and their legacy `omo-memorian:*` read aliases. |
| `memory-child-model-chain.ts` | Shared by the Kibitzer sidecar (`kibitzer/sidecar-model.ts`) and the facts child (`facts-in-process-launch.ts`): turns `{ model, fallbacks }` into `selectedModel` + `fallbackModels` (`ResolvedModelRecord`s keyed by the bare primary selector) and, only when fallbacks exist, `retry: { maxRetries: 1 }` so the engine rotates rungs after one same-model retry (~2 s) instead of the default ~62 s exponential backoff, which the sidecar's 90 s wake deadline could never absorb; a single-model category keeps the engine default. The beyond-category ladder is refused before the conversion (see `kibitzer/sidecar-model`). |
| `facts-wiring.ts` / `facts-runner.ts` / `facts-in-process-launch.ts` | Durable facts queue (settle-time enqueue, session-start reconcile) + quick-pinned background extractor child; the parent applies the whole batch under the `memory-write` lock, the child never touches git. `facts-in-process-launch` builds the in-process child with the same `childModelChainSpec` as the judge (remaining quick rungs + one-retry budget), so a mid-turn provider failure rotates rungs inside ONE child; its candidate loop advances only on launch-time `session_create_failed`, never re-launching rungs the engine already tried. |
| `facts-terminal-writes.ts` / `facts-run-finalize.ts` | Terminal outcome for a claimed run. Every failure path records the run's queued endpoints in the failure-streak ledger BEFORE writing `final.json`/`abandoned.json` (idempotent per `failureId` = runId, so a crash in that window replays safely); `committed`/`no_facts` clear those records after `markConsumed`. |
| `facts-run-storage.ts` / `facts-run-prune.ts` / `facts-run-cleanup.ts` | Run-dir lifecycle. Reservation (scan + mkdir + ledger) and retention pruning share `facts-runs.lock`, so pruning can never free a `facts-<digest>-<attempt>` name a reservation is probing; the attempt sequence starts above the highest name ever seen (tombstones included) and stays monotonic. Pruning keeps the newest terminal run ALWAYS, plus `keepLast=20` under a 128MiB total, skips any run whose finalize lock is busy, renames to `.prune-*` under both locks and `rm -rf`s only after releasing them. Session-start maintenance clears leftover payloads and `.prune-*` tombstones. |
| `facts-launch-selection.ts` | Launch-time read of `failures.json` (once per attempt) feeding memory-core's `selectLaunchable`. Fail-closed: an unreadable ledger warns and refuses the launch instead of degrading to "no failures". Parked/backoff endpoints are dropped, and a dropped entry blocks its own conversation's later entries; no run dir is reserved when the selection is empty. |
| `dream-selector.ts` | Dream conversation auto-selector: unreflected-volume gate, caps at `dream.auto_select_max` conversations / `dream.auto_select_max_chars` UTF-8 bytes. |
| `skills-usage.ts` | Per-skill read-count ledger consumed by the dream skill-audit phase. |
| `soul-notice.ts` | Soul-edit notice entry type + renderer: commits touching a `SOUL_PATHS` entry (`system/persona.md`, `system/identity.md`, `system/boundaries.md`) emit a non-model-facing `appendEntry` notice, gated by `soul.edit_notice`. |
| `memory-notice-wiring.ts` | Direct-surface commit notice. `onCommit(context, commit)` appends ONE `soul-updated` entry when the commit touches a soul path and `soul.edit_notice` resolves true for that identity; nothing else is emitted (the direct surface's own `renderResult` already draws the write row). Also exports the `omo-memory:write-updated` renderer reused by that row. The MCP receipt-file consumer was retired by the memory tool surface consolidation. |
| `shutdown-drain.ts` | Session-shutdown journal drain under a hard budget. The journal flush runs first, before the handler's pre-drain cleanup awaits, through the drain's `flushJournal` entry point; `wiring.onSessionShutdown` calls it before the Kibitzer sidecar shutdown and facts cancellation and then runs the remaining steps via `run(input, { journalFlushed })` without re-flushing. A journal flush skipped or starved by the budget raises the error-level `memory shutdown drain skipped the journal flush` alarm; optional-tail exhaustion keeps its info/warn lines. |
| `sandbox.ts` | Seatbelt/bwrap sandbox transforms for detached reflection and dream children (`memory.reflection.sandbox`: `required|auto|off`, default `auto`). The in-process kibitzer and facts runners never pass through it. |
| `kibitzer/` | Read-only recall judge surface: the resident in-process sidecar (one per bound main session, `kibitzer/index.ts`) fed every prompt / tool_call / tool_result as bounded, redacted events, waking only for a lexical recall candidate it has not judged (under a machine-wide wake lease, a per-wake tool budget and a 90 s deadline), reading through exactly five member-scoped read-only tools (`read`, `grep`, `session_entries`, `memory` with `search`/`read` only, `nudge`), reseeding itself before its context budget runs out, backing off after a failed child, and delivering nudges only through the `nudge` tool. It never writes memory. Every settled wake is one line of `recall/sidecars/<encoded-session>/wakes.ndjson`, and the third consecutive diagnostic failure of a session is the one `omo-kibitzer:gate` notice (`kibitzer/observe.ts`). See `kibitzer/AGENTS.md`. |
| `commands/` | Thirteen slash commands (`MEMORY_COMMAND_NAMES`: `/memory`, `/memfs`, `/remember`, `/init`, `/doctor`, `/recompile`, `/memory-repository`, `/sleeptime`, `/reflect`, `/dream`, `/search`, `/people`, `/facts`); read-only output never enters model context. `/facts retry [--conversation <id>]` is the ONLY unpark path; it never touches queue files or either watermark. See `commands/AGENTS.md`. |
| `palace/` | Self-contained HTML memory viewer. See `palace/AGENTS.md`. |
| `guard.ts` | Soft cross-identity guard via `tool_call` (file tools only; bash advisory-only). The verdict is structural - inside `agents/` or `transient-runs/` but outside the bound run's own root - with no directory enumeration, so per-call cost is constant in the number of identities on disk (#7765). |
| `policy-guard.ts` | Hard guard: registers a filesystem policy when the host exposes `registerFilesystemPolicy` (senpi >= feat/extension-fs-policy), soft guard otherwise. `check()` is structural over the same two areas; `deniedRoots` metadata lists only durable (repo-owning) sibling identities. That list is the policy's only filesystem read and `check` never consults it, so it is enumerated when a host first reads it - memoised from then on - instead of while the session binds (#8412). |
| `skills-scope.ts` | Agent memfs `skills/` exposure via `resources_discover`. On a host whose event carries `scopedEntries: true` the dir is contributed as `{ path, scope: "user" }` (memory skills are user data, not harness payload, so they stay in the compact startup banner under `user`); an older host gets the plain path because it would treat an object entry as a path string and abort session start. |
| `status.ts` | Footer status + committed-only token advisory at `compile_warn_tokens`. |
| `status-live.ts` | Generic footer animation: braille reflecting spinner, fingerprint-gated segment refresh, injectable timers. |
| `status-live-wiring.ts` | Binds the footer animation to memory state: session-to-identity resolution, git-backed fingerprint, segment line via the shared status.ts formatter. |
| `status-active-runs.ts` | Per-identity registry of in-flight reflection runs keyed by run id; drives footer animation and supplies run details to the rpc bridge. |
| `memory-rpc-bridge.ts` | RPC surface: fingerprint-deduped `omo.memory.updated` snapshot push plus `omo.memory.status` pull; every rpc touch is guarded, so a host without `pi.rpc` is a silent no-op. |
| `binding.ts` / `bindings/` | Binding entry record + renderer. |
| `capabilities.ts` | `appendEntry`/`registerEntryRenderer` capability narrowing (`MemoryExtensionAPI`). |
| `supervisor.ts` | Ref-counted module supervisor placeholder. |
## Declared divergences from letta-code@a75f4d93e
Every row is intentional; each was weighed against the research corpus (claim-graph.md).
1. **Local-capable matrix only.** Letta Cloud rows are out: org shared repositories, server block identity/sharing, server secrets, `.af` import/export, server-side tool management, semantic/vector search endpoints, per-user cloud metadata, server context accounting. The push-only git mirror (`/memory-repository`) is the cloud-free sync story.
2. **No mods-in-memory.** `mods/` executables in the memory repo are not loaded (trusted code in memory expands attack surface). The repo layout tolerates a `mods/` dir but nothing executes it.
3. **No reflection arena, no channels.** The A/B arena experiment and Discord/Telegram `/reflection` routing have no omo analog.
4. **Local search is text-only by design**, matching letta's local backend (its `vector|hybrid` modes degrade to FTS-lite locally). Senpi sessions are scanned via the senpi JSONL provider; archived-sidecar and internal-session exclusions apply, `--include-hidden` overrides.
5. **No mid-conversation `<memory_update>` one-shot.** Letta special-cases `anthropic/claude-opus-4-8` (C15/C27); omo recompiles per run for every model (generalized, per-run `before_agent_start` re-check of HEAD).
6. **No `/reflect --auto` selector subagent, no external-transcript staging, no `letta dream --to` doc maintenance.** Manual reflection takes `--recent N` / `--conversation <ids>` / free-text focus. The dream pass (below) carries its own conversation auto-selector, but that is an omo extension riding the reservation machine, not a port of letta's slash surfaces.
7. **Recall is a resident judge, not letta's conversation-bootstrap injection.** There is no bootstrap-time injection and no lexical auto-injection: each bound main session owns ONE resident, read-only, quick-pinned in-process Kibitzer sidecar (senpi-task InProcessRunner, no subprocess) that receives every prompt / `tool_call` / `tool_result` as a bounded, redacted event and is woken - at a prompt or `tool_call` hook, never at settle - only when the turn's lexical candidates (user text plus tool-argument tokens) contain a memory path it has not judged this lifetime. It answers only through the in-process `nudge` tool; its other four tools (`read`, `grep`, `session_entries`, read-only `memory`) let it check before it speaks, and it has no way to write memory. When the agent is still streaming, validated nudges land at the next tool boundary of the SAME turn as one hidden steer message; anything still held when the turn ends is injected on the NEXT prompt. An idle session is never woken for a nudge alone. `/search` remains the manual recall surface (letta's local path already disables AI description generation, C46).
8. **No onboarding tutorial personality / welcome hints.** Default seeds (`system/persona.md`, `system/human.md`) are the only first-run content.
9. **Reflection sandbox default is `auto`**, not letta's fail-closed `required` (C33): default-on reflection must not break hosts without seatbelt/bwrap. `memory.reflection.sandbox: "required"` restores letta semantics.
10. **`memory_description`/`limit` frontmatter tolerance matches letta; block-scalar descriptions are rejected** (letta's cut-prefix accepted `>`; that acceptance is treated as a bug). Frontmatter additionally parses and preserves typed `kind` + `aliases` for people cards, an omo extension letta has no analog for.
11. **str_replace replaces the FIRST occurrence** (letta actual behavior, C21); the advisory's exactly-one-match proposal was rejected for parity.
12. **Message store = senpi session JSONL** (letta's LocalStore JSONL was not ported; the engine reads senpi's native format).
13. **`reflection.trigger.step_count` stays at letta's 25** (schema default); memory v2 changed the trigger machinery and added the dream kind, not the reflection cadence.
## Extensions beyond parity
Active learning is ON by default (`memory.enabled: true` plus every sub-feature below defaulting on). These are omo-only additions, not parity claims against letta-code:
- **Save nudge** (`memory.nudge`): after `every_user_turns` (default 10) accepted user turns without a memory write, a nudge line joins the compiled metadata block. Write detection keys on commit trailers (`Omo-Writer`/`Omo-Session`/`Omo-Turn`), never on git author identity.
- **Facts extractor** (`memory.facts`): settle-time queue entries debounce (`debounce_settles`, default 4) into an in-process child using the shared memory-child helper and `record_fact`; it emits the same `extraction.jsonl` run artifact; the parent applies the batch as one commit with a `Generated-By: facts-extractor` trailer. Shutdown leaves queued entries; the next session_start reconcile path relaunches them.
+ - **Oversized facts entry** (#8984, `facts-oversized-budget.ts` / `facts-oversized-child.ts`): ordinary batches stay under memory-core's 128 KiB `MAX_FACTS_PAYLOAD_BYTES`. A single entry above it is no longer only parked: once no ordinary batch remains, `facts-runner.ts` launches ONE complete entry (never split or truncated) when its serialized payload is at most 512 KiB (`FACTS_OVERSIZED_BYTES`) and the pinned model's known `contextWindow` fits the full context plus the output reserve. That run has a fixed budget: at most 8 provider requests (`FACTS_REQUEST_LIMIT`), each capped at 4,096 output tokens (`FACTS_OUTPUT_TOKENS`), `retry.maxRetries: 0` and no fallback rungs, auto-compaction off, and at most 128 KiB / 256 accepted records (`FACTS_RECORD_BYTES` / `FACTS_RECORD_LIMIT`, reserved before each append). Compaction, `resume_context_reduced`, a `length` stop, a model swap or any budget overflow fails the whole run, and the original queue and watermarks are kept with zero commits; the queue is consumed only after the usual receipt/apply/commit. Entries above 512 KiB, or whose model capacity is unknown or too small, are parked as before.
- **Dream pass** (`memory.dream`): a distinct reservation trigger kind (`"dream"` with `origin: manual|idle|shutdown|pressure`) running a consolidation + skill-audit + people persona through the same worker pipeline. Idle/shutdown use the conversation auto-selector capped by `auto_select_max` / `auto_select_max_chars`; pressure compacts the memory tree without a transcript-volume floor.
- **People cards** (`memory.people`): typed card + observation formats in memory-core (`kind`/`aliases` frontmatter, per-card `max_entries`/`max_entry_chars` limits), plus a relationship graph view in the memory palace.
- **Soul v2** (`memory.soul`): `system/identity.md` joins `<self>` alongside the persona, and committed soul edits surface a visible non-model-facing notice when `edit_notice` is true.
- **Run supervisor**: every reflection/dream child is spawned through `memory-run-supervisor.mjs`, which owns the run identity handshake and the `outcome.json` sentinel. On win32, process groups and POSIX signals are unavailable: the child spawns non-detached, graceful `child.kill()` fires at the SIGTERM instant and `taskkill /pid <pid> /T /F` at the SIGKILL instant, process-start identity is always null so reconciliation of an abruptly dead supervisor resolves through the non-destructive UNKNOWN path to `abandoned.json`, and the bootstrap self-enforces the same absolute deadline. Facts do not use this supervisor.
## Deliberate constants (not knobs)
- **Facts extractor category is pinned `"quick"`** (`facts-runner.ts` `QUICK_CATEGORY`); resolution failure logs a warning and skips the run, never falls back to another category. The schema comment in `omo-config-core/src/schema/memory.ts` records the same decision.
- **Shutdown drain budget is 1500 ms** (`shutdown-drain.ts` `SESSION_SHUTDOWN_DRAIN_BUDGET_MS`): senpi blocks shutdown on the drain handler, so the budget is a pinned constant rather than configuration.
- **Automatic reflection failures back off 5 s doubling to 5 min** (memory-core `journal/store.ts` `recordReflectionFailure`, called from `reflection/reservation.ts` for non-manual, non-dream outcomes other than `merged`/`no_changes`; `reflection/machine.ts` `evaluateTransitions` reserves nothing before `next_eligible_at`). A manual `/reflect` bypasses the window and a successful reflection resets `consecutive_failures`/`next_eligible_at`. Not a knob.
- **Automatic reflection parks after 3 consecutive non-retryable or 6 consecutive retryable failures and probes once every 6 h while parked** (memory-core `reflection/park.ts`, state in `runtime/reflection/park.json` written by `ReflectionReservationStore` under the scheduler lock). `complete()` takes the adapter's classification (`worker/failure-policy.ts`: model/auth/ENOENT/sandbox/validation failures are non-retryable; rate limits, provider outages, timeouts, parent_dirty and merge_conflict are retryable). While parked, `evaluate`/`tryReserve` answer automatic requests (step-count, compaction, dream idle/pressure/shutdown) with status `parked` and the next probe time; manual `/reflect` and manual `/dream` bypass the gate, and any merged/no_changes clears the park. The park edge is announced once per session (`worker/park-alert.ts`), the footer shows ` paused`, and `/doctor` reflection-health names the pause and the manual retry. Not a knob.
- **Kibitzer wake deadline is 90 s, reseed fires at 60% of `sidecar_max_tokens`, accepted-nudge cooldown is 2 wakes per 10 min per session, child backoff is 1 s doubling to 5 min** (`kibitzer/sidecar.ts`, `kibitzer/wake-policy.ts`): the configurable recall knobs are `category`, `max_items`, `event_caps`, `sidecar_max_tokens`, `max_concurrent_wakes` and `tool_budget`; everything else about the sidecar's pacing is pinned.
- **Pending reflection captures are bounded to 32 conversations and 4 MiB** (memory-core `reflection/machine.ts`: `REFLECTION_PENDING_MAX_CONVERSATIONS` / `REFLECTION_PENDING_MAX_BYTES`). First admission, merges, and legacy promotion evict whole conversations in first-seen order, preserving request priority and leaving evicted journal cursors retryable. The byte budget counts UTF-8 JSON and nesting whitespace, not just transcript characters; a single oversized capture is evicted too. The limits allow roughly 32 normal 128 KiB capture windows without retaining an unbounded cross-session backlog.