diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b99f48..8d17bcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -190,6 +190,80 @@ browser and read back as an image. Recorded because it generalises: `mermaid.parse()` proves syntax and says nothing about layout, so a diagram is unverified until someone has looked at it. +### … and rendering it in *my* browser was still not enough + +Reported from a real viewer: several boxes had their bottom line of text sliced +off. Reproduced and root-caused rather than nudged — **Mermaid measures a node +label with its own font metrics, computes the box, then renders the label as real +HTML inside a ``.** Any host stylesheet that touches the +`line-height` or `font-size` of that HTML makes the text taller than the box +already committed to, and the overflow is clipped at the box edge. Error +accumulates per line, so the loss always lands on the last line of the tallest +labels — which is exactly what was reported. + +Two fixes were tried and only the second works: + +- `%%{init: {'flowchart': {'htmlLabels': false}}}%%` — **rejected, and verified + ineffective rather than assumed so.** The directive *is* honoured (label + elements switch from 16 `foreignObject` to 7 `tspan`), and the clipping is + identical, because the inflated font-size still inherits into SVG text. +- **A hard limit of two short lines per node, with the detail moved into the prose + under each diagram.** One- and two-line boxes have enough vertical slack to + absorb the inflation; three- and four-line boxes do not. This is also better + documentation — the old nodes were carrying paragraph-sized text. + +The regression harness is now the interesting artefact: render every block with a +deliberately inflated `line-height: 1.7 !important` on the label HTML, screenshot, +and read it. Two survivors of the rewrite were caught only by that harness — a +long unbreakable `/opt/pi-observational-memory` path silently wrapping to a third +line, and a cylinder (`[( )]`) shape, whose curved bottom leaves less room than a +rectangle for the same two lines. + +### §4 answers the question the document left hanging: what compaction does to your context + +Asked directly and worth writing down: *if the old conversation is folded away, is +the session back to knowing nothing?* No — and the specifics are all checkable +against pi 0.84.3's own `docs/compaction.md` and the extension's source: + +- **A verbatim tail survives, sized by a token budget rather than a message + count.** Pi walks back from the newest entry until `keepRecentTokens` [20000], + and everything from that `firstKeptEntryId` onward is kept **unchanged**. Cut + points land on turn boundaries, never mid-tool-call. +- **The system prompt and `AGENTS.md` are not in the compacted region at all** — + they are rebuilt from disk on every request, so compaction cannot lose them. +- **Nothing is deleted from disk.** Compaction *appends* a `compaction` entry + carrying the summary and the cut pointer; no session line is rewritten in place. +- **`recall` therefore still resolves ids whose sources left the context**, because + it reads the full branch via `sessionManager.getBranch()` and never consults the + context window. +- **Repeated compaction does not summarise the summary.** The text is always + rendered from live observation/reflection records, so there is no + generation-loss spiral; the projection is incremental against the last full-fold + boundary and escalates to a true re-fold from the branch root at + `observationsPoolMaxTokens` [20000]. + +And one correction to this repo's own earlier claim: **"compaction calls no model" +is a steady-state property, not an absolute.** If the ledger is empty — compaction +firing before the observer has ever run — the hook returns nothing and explicitly +declines ownership (`// Decline ownership so Pi's native summarizer preserves the +pre-cut context.`), and pi's own model-based summariser runs. The doc now says so, +with the snippet. + +### A shipped doc bug: the ledger entry type was stated exactly backwards + +§9 told readers the entries are `custom_message` and specifically *not* `custom`. +It is the other way round, so the one grep the section existed to get right was +the one it got wrong. Corrected against the live session file — 11 +`om.observations.recorded` and 6 `om.reflections.recorded` entries, all +`"type":"custom"`, alongside `"type":"custom_message"` entries whose `customType` +is `mempalace-mailbox` and `mempalace-wakeup`, which is precisely where the +confusion came from: **the mailbox uses the context-visible API, om's ledger uses +the invisible one.** + +That is not a typo but a load-bearing distinction, and the fix turns it into a +feature the doc now advertises: `custom` entries *"do not participate in LLM +context"* (pi `docs/session-format.md`), so **the ledger costs zero context until +it is folded** — now a row in the cost table. ### Not covered by any of this The opencode bridge is a separate write path the feeder hook never sees, and the diff --git a/docs/observational-memory.md b/docs/observational-memory.md index 3778370..ddc8c03 100644 --- a/docs/observational-memory.md +++ b/docs/observational-memory.md @@ -11,16 +11,18 @@ leave any of it switched on. [`how-it-works.md`](https://github.com/elpapi42/pi-observational-memory/blob/main/docs/how-it-works.md) (hooks and internals) and [`configuration.md`](https://github.com/elpapi42/pi-observational-memory/blob/main/docs/configuration.md) -(every setting). Those are normative; this document is the **deployment** view — -what is pinned here, how it is wired, what it costs, and how it differs from -MemPalace. For the palace, see +(every setting). Pi's own compaction mechanics are in +`/usr/lib/node_modules/@earendil-works/pi-coding-agent/docs/compaction.md`. +Those are normative; this document is the **deployment** view — what is pinned +here, how it is wired, what it costs, and how it differs from MemPalace. For the +palace, see [`mempalace-toolkit/docs/fleet-memory.md`](https://gitea.jordbo.se/joakimp/mempalace-toolkit/src/branch/main/docs/fleet-memory.md). > Verified on pi-devbox **v1.8.9** (`release_tag v1.8.9`, source `aac4a1c`), > which bakes pi-observational-memory **v3.0.4** at commit `ce9fc98` — the value > in `/etc/pi-devbox/build-manifest.json` → `components.pi-observational-memory`. -> Every number below was read from that tree or from the live container, not -> from release notes. +> Every number below was read from that tree, from pi 0.84.3's own docs, or from +> the live container. --- @@ -30,7 +32,7 @@ A long pi session outgrows the model's context window. Pi's answer is **compaction**: fold the older part of the conversation into a summary and keep recent messages verbatim. That is unavoidable, and it is where sessions go wrong — the summary is produced *at the moment of pressure*, by a model, about a -transcript that is about to be discarded. +transcript that is about to leave the context. Observational memory changes *when* the remembering happens. Instead of summarising in a panic at the end, it keeps a small **ledger** up to date while @@ -38,15 +40,21 @@ the session runs, and compaction then just folds that ledger. ```mermaid flowchart LR - A0["plain compaction"] --> A1["long session,
context filling"] - A1 --> A2["a model summarises
the transcript
at the moment of pressure"] - A2 --> A3["one prose summary:
detail chosen in a hurry,
no route back to the original"] - B0["with observational memory"] --> B1["long session,
context filling"] - B1 --> B2["while you work:
a cheap background model appends
observations + reflections
to a ledger"] - B2 --> B3["compaction folds the ledger
no model call"] - B3 --> B4["every item keeps a 12-hex id —
recall(id) returns
the exact source"] + A0["plain compaction"] --> A1["context fills"] + A1 --> A2["a model summarises
under pressure"] + A2 --> A3["prose summary,
no way back"] + B0["with observational
memory"] --> B1["context fills"] + B1 --> B2["ledger written
as you work"] + B2 --> B3["compaction folds
the ledger"] + B3 --> B4["ids you can
recall"] ``` +Top row is pi on its own: one model call at the worst possible moment, detail +chosen in a hurry, and the original wording gone from view. Bottom row is this +image's default: the thinking happened earlier on a cheap model, the fold is +deterministic, and every line in the result carries an id that resolves back to +the exact source. + ## 2. The mental model: three layers and a ledger | Layer | What it is | Example | @@ -55,14 +63,20 @@ flowchart LR | **Reflection** | a durable conclusion *backed by* observations | "the user optimises for avoiding 67-minute rebuilds" | | **Drop** | a tombstone retiring an observation from active memory | the superseded detail of a bug that is now fixed | -These are appended as silent ledger entries (`om.observations.recorded`, -`om.reflections.recorded`, `om.observations.dropped`) and **folded** — replayed -from the branch root — to produce the memory state. The ledger is the source of -truth; the compaction summary is a rendering of it. +These are appended to the session as silent ledger entries +(`om.observations.recorded`, `om.reflections.recorded`, +`om.observations.dropped`) and **folded** — replayed in order — to produce the +memory state. The ledger is the source of truth; what you see in a compacted +session is a rendering of it. -Memory is **branch-local**. A pi session is a tree (resume, fork), and the fold -follows the current branch only, so a forked branch does not inherit another -branch's view. +Two properties follow, and both matter later: + +- **The ledger itself costs no context.** Those entries are pi `custom` entries, + which *"do not participate in LLM context"* (pi `docs/session-format.md`). They + sit in the session file and reach the model only via the fold at compaction. +- **Memory is branch-local.** A pi session is a tree (resume, fork), and the fold + follows the current branch only, so a forked branch does not inherit another + branch's view. ## 3. The lifecycle @@ -71,58 +85,133 @@ progress* rather than wall-clock time. Defaults in brackets. ```mermaid flowchart TD - T(["turn_end"]) --> O{"tokens since last
observation coverage
≥ observeAfterTokens
[10000]?"} - O -- yes --> OBS["observer runs:
appends om.observations.recorded"] - O -- "no (observer not due)" --> R{"tokens since last
reflection
≥ reflectAfterTokens
[20000]?"} - R -- yes --> REF["reflector runs:
appends om.reflections.recorded"] - REF -- "non-empty reflection AND active pool
> observationsPoolTargetTokens [10000]" --> DR["dropper runs:
appends om.observations.dropped"] - S(["agent_settled"]) --> C{"tokens since last compaction
≥ compactAfterTokens [81000]
and pi is idle?"} - C -- yes --> CP["extension calls ctx.compact()"] + T(["turn_end"]) --> O{"10k raw tokens
since observing?"} + O -- yes --> OBS["observer runs"] + O -- "no" --> R{"20k tokens
since reflecting?"} + R -- yes --> REF["reflector runs"] + REF -- "if pool over 10k" --> DR["dropper prunes"] + S(["agent_settled"]) --> C{"81k tokens
since compacting?"} + C -- yes --> CP["ctx.compact()"] CP --> H(["session_before_compact"]) - H --> F["deterministic fold of the ledger:
no model call, no waiting on workers"] - F --> VIS["compacted memory the agent reads"] + H --> F["fold the ledger
no model call"] + F --> VIS["compacted memory"] ``` -Two details worth carrying, because they are easy to get wrong: - -- **The dropper has no clock of its own.** It is post-reflection maintenance, - gated on a *successful same-turn* reflection — not a third worker on a third +- **observer** — `observeAfterTokens` [10000]: writes observations for the + conversation it has not covered yet. +- **reflector** — `reflectAfterTokens` [20000]: promotes patterns across + observations into durable reflections. +- **dropper** — no clock of its own. It is post-reflection maintenance, gated on + a *successful same-turn* reflection **and** an active pool above + `observationsPoolTargetTokens` [10000]. Not a third worker on a third threshold. -- **Compaction itself calls no model.** That is the whole design: the expensive - thinking happened earlier, in the background, on a cheap model. +- **compaction** — `compactAfterTokens` [81000], checked when pi goes idle, so it + never interrupts a turn. Pi will also compact on its own when the context is + nearly full (`contextTokens > contextWindow - reserveTokens`, `reserveTokens` + [16384]). -## 4. What you actually get +## 4. What compaction actually does to your context -- **Compaction stops being a stall.** The latency path is deterministic work on - ledger entries, not a summarisation call. +This is the question the rest of the document used to leave hanging: if the old +conversation is folded away, is the session back to knowing nothing? + +**No.** Compaction replaces *part* of the context, not all of it, and it deletes +nothing at all from disk. + +```mermaid +flowchart LR + SYS["system prompt
+ AGENTS.md"] --> CTX["what the model sees
on the next turn"] + SUM["folded memory:
reflections + observations"] --> CTX + TAIL["recent turns,
verbatim"] --> CTX + DISK[("session .jsonl: all of it")] -. "recall(id)" .-> CTX +``` + +Where each piece comes from: + +- **System prompt and `AGENTS.md` — never compacted, because they were never + conversation.** Pi rebuilds them from disk on every request + (`loadContextFileFromDir`), so they cannot be lost by compaction. +- **The verbatim tail — sized by a token budget, not a message count.** Pi walks + backwards from the newest entry accumulating token estimates until + `keepRecentTokens` [20000] is reached; that entry becomes `firstKeptEntryId`, + and *everything from there on is kept unchanged*. Cut points land on turn + boundaries, never mid-tool-call. So the most recent ~20k tokens of real work — + your last instructions, the diffs, the test output — survive word for word. +- **The folded memory — replaces only what came before that cut.** Rendered from + the ledger's records: reflections and observations, each with its 12-hex id. +- **The session file — untouched.** Compaction *appends* a `compaction` entry + (`{"type":"compaction", summary, firstKeptEntryId, tokensBefore, …}`) and + rebuilds context from it on later turns. Nothing is rewritten in place; the + only documented way to remove session content is deleting the whole `.jsonl`. + +That last point is what makes the answer to "is the detail gone?" *no* rather +than *mostly*: `recall` does not read the context window at all. It calls +`sessionManager.getBranch()` — the full branch from the root — and resolves an +observation id back to the original entries. Detail that left the model's view +an hour ago is still one `recall` away. + +**Repeated compaction does not summarise the summary.** The rendered text is +always built from live observation/reflection *records*, never from the previous +compaction's prose, so there is no generation-loss spiral. (Mechanically the +projection is incremental — it re-derives back to the last full-fold boundary and +carries the rest forward, escalating to a genuine re-fold from the branch root +when the observation pool reaches `observationsPoolMaxTokens` [20000].) + +So the honest summary of the state after compaction: **the model keeps its +instructions, keeps recent work verbatim, trades older turns for a dense +id-carrying digest of them, and can pull any of it back on demand.** Not a fresh +start — a smaller, cheaper, still-navigable one. + +### One caveat about "no model call" + +If the ledger is empty — compaction fires before the observer has ever run — the +hook returns nothing and *declines ownership*, and pi's own model-based +summariser runs instead: + +```ts +const summary = renderSummary(projection.reflections, projection.observations); +if (summary.length === 0) { + // Decline ownership so Pi's native summarizer preserves the pre-cut context. + return; +} +``` + +In steady state (any session old enough to have produced one observation) om's +hook wins and compaction is model-free. "Never calls a model" is true in practice +and false in principle; the fallback is deliberate, so an empty ledger degrades +to normal pi rather than to no summary at all. + +## 5. What you actually get + +- **Compaction stops being a stall.** In steady state the latency path is + deterministic work over ledger entries, not a summarisation call. - **Nothing important vanishes silently.** Compaction is lossy by design, but - every observation and reflection keeps a 12-character id, and `recall()` - returns the exact evidence behind it — the original wording, the reasoning, - the file path, the error text. + every item keeps a 12-character id, and `recall()` returns the exact + evidence — original wording, reasoning, file path, error text. - **The bookkeeping runs on a cheaper model than your session.** In this image - that is deliberate and visible (§6): background workers on Haiku, the session - on Opus. -- **It is automatic.** There is no habit to maintain, unlike the palace protocol - — which is exactly why the two systems complement each other (§10). + that is deliberate and visible (§7): background workers on Haiku, session on + Opus. +- **It is automatic.** No habit to maintain, unlike the palace protocol — which is + exactly why the two complement each other (§11). - **Forks stay clean.** Branch-local memory means a `fork` sub-agent's noise does not leak into the parent's folded memory. -## 5. `recall` is not a search tool +## 6. `recall` is not a search tool `recall` takes **one specific 12-hex id** that already appears in compacted -memory or in `/om:view`, and looks it up in the ledger on the current branch. It -cannot be given a topic. It can return an observation (marked `active` or -`dropped`), or a reflection together with the observations supporting it. +memory or in `/om:view`. It cannot be given a topic. It can return an observation +(marked `active` or `dropped`), or a reflection together with the observations +supporting it. ```mermaid sequenceDiagram participant M as compacted memory participant A as agent - participant L as ledger (current branch) + participant L as ledger M->>A: "[high] user rejected option B (a1b2c3d4e5f6)" A->>L: recall("a1b2c3d4e5f6") - L-->>A: exact observation + its source entry ids - Note over A: acts on the original wording,
not on the compressed paraphrase + L-->>A: exact observation + source ids + Note over A: acts on the original wording ``` The rule of thumb the agent skill uses: recall **before a load-bearing action** @@ -130,30 +219,30 @@ that rests on a compressed memory — shipping a change, asserting a fact, answering "why do you believe that". One recall is cheap; redoing finished work is not. -## 6. How it is wired in this image +## 7. How it is wired in this image ```mermaid flowchart TB - IMG["image layer
/opt/pi-observational-memory
v3.0.4 @ ce9fc98 (pinned)"] --> REG["~/.pi/agent/settings.json
packages[]: ../../../../opt/pi-observational-memory
the only source of truth for which copy loads"] - REG --> SESS["your pi session"] - SESS -->|"your turns"| SM["session model
claude-opus-5"] - SESS -->|"observer / reflector / dropper"| WM["memory model
claude-haiku-4-5 (Bedrock)"] - SESS -->|"ledger entries (custom_message)"| JL["~/.pi/agent/sessions/<project>/<timestamp>_<uuid>.jsonl"] - JL --> VOL[("devbox-pi-config volume — docker-compose.yml:59
survives --force-recreate")] + IMG["baked in the image:
v3.0.4 @ ce9fc98"] --> REG["settings.json
packages[]"] + REG --> SESS["your pi session"] + SESS -- "your turns" --> SM["session model:
Opus"] + SESS -- "observer, reflector,
dropper" --> WM["memory model:
Haiku"] + SESS -- "ledger entries" --> JL["session .jsonl"] + JL --> VOL[("devbox-pi-config
volume")] ``` Four consequences of that wiring: 1. **There is no separate database.** Memory *is* entries inside the ordinary pi - session `.jsonl`. Nothing extra to back up, and nothing to migrate. -2. **It survives container recreate**, because `~/.pi` is the - `devbox-pi-config` named volume — the same one that keeps your pi config and - sessions. -3. **`packages[]` is the only source of truth for which copy is loaded.** A - clone at `/workspace/pi-observational-memory` may exist (and today matches - `/opt` byte-for-byte at `ce9fc98`) — its presence proves nothing. If you ever - run a patched build, you point `packages[]` at it explicitly and start a new - session. + session file (`~/.pi/agent/sessions//_.jsonl`). + Nothing extra to back up, nothing to migrate. +2. **It survives container recreate**, because `~/.pi` is the `devbox-pi-config` + named volume (`docker-compose.yml`) — the same one holding your pi config and + session history. +3. **`packages[]` is the only source of truth for which copy is loaded.** A clone + at `/workspace/pi-observational-memory` may exist (and today matches `/opt` + byte-for-byte at `ce9fc98`) — its presence proves nothing. To run a patched + build you point `packages[]` at it explicitly and start a new session. 4. **The worker model is a deliberate choice, and it is yours to change.** The seeded config sends background work to Haiku while your session runs Opus: @@ -164,19 +253,20 @@ Four consequences of that wiring: } ``` -## 7. What it costs +## 8. What it costs | Resource | Cost | |---|---| | Model calls | up to **three** background calls per consolidation pass (observer, reflector, dropper), each capped at `agentMaxTurns` [16], on the configured memory model — not your session model | -| Latency in your turns | none by construction: workers run from `turn_end`, and the compaction hook does no model work | +| Latency in your turns | none by construction: workers run from `turn_end`, compaction runs when pi is idle, and the fold itself does no model work | | Disk | negligible — JSON lines inside a session file that would exist anyway (measured here: `~/.pi/agent/sessions` = 30 MB total, tens of `om.*` entries per session) | +| Context window | **zero until compaction.** `custom` entries do not enter LLM context; only the folded summary does | | Attention | none once configured; there is no protocol for you or the agent to remember | -If that is still more than you want on a given run, §8's `passive` switch turns +If that is still more than you want on a given run, §9's `passive` switch turns off all proactive work while keeping `recall` and `/om:*` usable. -## 8. Configuration +## 9. Configuration Global: `~/.pi/agent/settings.json` (persisted in the volume). Per project: `/.pi/settings.json`, which overrides global. Precedence is @@ -188,7 +278,7 @@ project → global → environment, and the environment can only override `passi | `reflectAfterTokens` | `20000` | reflector cadence (and thereby dropper opportunities) | | `observerChunkMaxTokens` | 20% of the memory model's context window, else `60000` | cap on one observer run's input | | `compactAfterTokens` | `81000` | when proactive auto-compaction fires | -| `observationsPoolMaxTokens` | `20000` | pressure at which compaction does a full fold | +| `observationsPoolMaxTokens` | `20000` | pool size at which compaction does a full re-fold from the branch root | | `observationsPoolTargetTokens` | half of max (`10000`) | what the dropper aims back down to | | `agentMaxTurns` | `16` | shared turn cap for the three workers | | `model` | unset → session model | send background work to a cheaper/faster model | @@ -196,6 +286,10 @@ project → global → environment, and the environment can only override `passi | `passive` | `false` | **kill switch** for all proactive background work; `recall` and `/om:*` still work | | `debugLog` | `false` | per-session NDJSON trace at `~/.pi/agent/observational-memory/debug/.ndjson` | +Pi's own compaction knobs live under a separate `compaction` key — +`keepRecentTokens` [20000] sets the verbatim tail from §4, `reserveTokens` +[16384] the headroom that triggers pi's own compaction. + One-off passive run, no config edit: ```bash @@ -206,7 +300,7 @@ Invalid values are ignored rather than fatal, so a typo degrades to the default instead of breaking your session — which also means a typo is silent. Check with `/om:status`. -## 9. Confirming it is actually working +## 10. Confirming it is actually working Do not infer health from the absence of a warning; look: @@ -216,34 +310,36 @@ Do not infer health from the absence of a warning; look: /om:view # what the agent currently sees /om:view full # full ledger truth at the branch tip -# 2. from a shell — are ledger entries being written? +# 2. from a shell — are ledger entries being written, and has it compacted? grep -o '"customType":"om\.[a-z.]*"' \ "$(ls -t ~/.pi/agent/sessions/*/*.jsonl | head -1)" | sort | uniq -c +grep -c '"type":"compaction"' "$(ls -t ~/.pi/agent/sessions/*/*.jsonl | head -1)" # 3. which copy is loaded, and at what commit python3 -c "import json;print(json.load(open('$HOME/.pi/agent/settings.json'))['packages'])" git -c safe.directory=/opt/pi-observational-memory -C /opt/pi-observational-memory rev-parse HEAD ``` -Note the entry type in the transcript is `custom_message` (with -`customType: "om.…"`), not `custom` — an easy grep to get wrong and conclude -nothing is happening. +Ledger entries are `"type":"custom"` with `"customType":"om.…"`. Do not grep for +`custom_message` — that is a *different* pi API for entries that **do** enter LLM +context, used here by the MemPalace mailbox (`customType: "mempalace-mailbox"`), +not by om. -## 10. It is not the same thing as MemPalace +## 11. It is not the same thing as MemPalace -Both are called "memory" and they solve different problems. Nothing is wrong -with running both — this image does, and they cover each other's failure modes. +Both are called "memory" and they solve different problems. Nothing is wrong with +running both — this image does, and they cover each other's failure modes. ```mermaid flowchart LR - O0["observational memory"] --> O1["horizon:
this session"] - O1 --> O2["scope:
one branch, one machine"] - O2 --> O3["automatic —
no habit required"] - O3 --> O4["retrieval:
an id you already hold
→ recall"] - P0["MemPalace"] --> P1["horizon:
sessions, months, machines"] - P1 --> P2["scope:
the whole fleet, every harness"] - P2 --> P3["protocol-driven —
search first, diary at the end"] - P3 --> P4["retrieval:
semantic search, entity+time,
mailbox"] + O0["observational
memory"] --> O1["horizon:
this session"] + O1 --> O2["scope: one branch,
one machine"] + O2 --> O3["automatic"] + O3 --> O4["retrieval:
recall(id)"] + P0["MemPalace"] --> P1["horizon: months,
machines"] + P1 --> P2["scope:
the fleet"] + P2 --> P3["protocol-driven"] + P3 --> P4["retrieval:
search, KG, mailbox"] ``` | Question | Answer | @@ -255,18 +351,21 @@ flowchart LR | "Why is compaction not losing my session?" | observational memory | The crisp version: **observational memory keeps a session coherent; the palace -keeps the fleet coherent.** A container recreate wipes neither — but only -because `~/.pi` and the palace both live outside the container filesystem. +keeps the fleet coherent.** A container recreate wipes neither — but only because +`~/.pi` and the palace both live outside the container filesystem. -## 11. Gotchas +## 12. Gotchas - **Branch-local means branch-local.** Resuming or forking changes which ledger is folded. Memory that "disappeared" is usually on another branch. - **`recall` needs an id, not a topic.** If you only have a topic, that is a palace search, not a recall. -- **A `/workspace` clone is not evidence of what is loaded** — see §6.3. +- **A `/workspace` clone is not evidence of what is loaded** — see §7.3. - **`showWorkerNotifications: true` is not proof of work**; it reports runs, and an observer that deliberately emits nothing writes no ledger entry and simply retries after another `observeAfterTokens`. -- **`git log` in the baked tree needs `safe.directory`** (`/opt` is - root-owned): `git -c safe.directory=/opt/pi-observational-memory -C /opt/pi-observational-memory log`. +- **A turn bigger than `keepRecentTokens` splits.** The cut then lands mid-turn at + an assistant message and pi merges two summaries — rare, but it is why a very + large single turn can lose more verbatim detail than you would expect. +- **`git log` in the baked tree needs `safe.directory`** (`/opt` is root-owned): + `git -c safe.directory=/opt/pi-observational-memory -C /opt/pi-observational-memory log`.