Merge pull request #578 from AssetOverflow/audit/l10-l11-lookback

audit(l10-l11): lookback review of the lived spine #567–#573
This commit is contained in:
Shay 2026-06-05 16:08:18 -07:00 committed by GitHub
commit 4c4e15bb9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -0,0 +1,256 @@
# L10/L11 Lived-Spine Lookback Review (#567-#573)
Date: 2026-06-05
Branch: `audit/l10-l11-lookback`
Base reviewed: `origin/main` at `ea503f6d5177`
Scope: PRs #567-#573, the L10/L11 lived-spine stack.
## Executive Finding
The stack landed the intended Shape B+ resume spine: bit-exact field/vault/session
serialization, schema-v2 state loading, reboot-transparent L10 traces, EngineIdentity
lineage stamping, and proposal-only idle learning. The strongest wrong=0 hazards
requested in the brief are solid: `idle_tick()` does not ratify or bypass HITL,
vault restore does not reproject, and persistence restore adds no hidden field
repair at a forbidden site.
One live crash-consistency hazard remains: the new multi-file checkpoint treats
`manifest.json` as the final commit marker but loads `session_state.json`
unconditionally. A power loss after `session_state.json` is atomically replaced
and before `manifest.json` is replaced can pair an old manifest with newer lived
state on reboot. Fix before the next phase.
## Hazards
### H1 - Multi-file checkpoint can restore mixed-generation state
Category: hazard
Evidence:
- `ChatRuntime.checkpoint_engine_state()` writes recognizers, candidates, then
`session_state.json`, then `manifest.json` as the stated commit marker
(`chat/runtime.py:779-803`).
- `EngineStateStore.save_session_state()` explicitly says session state is saved
before the manifest so the manifest is the last durable act
(`engine_state/__init__.py:225-236`).
- On load, `_load_engine_state()` reads the manifest turn count, then restores
`session_state.json` whenever `persist_session_state=True` and the file exists;
there is no generation, digest, or turn-count check tying the loaded session
snapshot to the manifest (`chat/runtime.py:719-755`,
`engine_state/__init__.py:238-251`).
- The P4 tests cover an orphan temp file and `os.replace` failure for
`manifest.json`, but they only assert recovered `turn_count` from the manifest;
they do not simulate a newer committed `session_state.json` with an older
manifest (`tests/test_l10_continuity.py:204-238`).
Impact:
A kill between `save_session_state()` and `save_manifest()` can next-boot into a
hybrid checkpoint: old manifest/turn count plus newer field/vault/referent state.
That violates the L10 P4 claim that recovery lands on a valid prior checkpoint.
It is not a versor-condition failure, but it is a lived-spine continuity and
deterministic replay hazard.
Fix before next phase:
Make the checkpoint generation atomic at the composed-state level. Minimal repair:
stamp `session_state.json` with the same committed `turn_count`/generation and
load it only when it matches the manifest. Stronger repair: write a generation
directory or content-addressed bundle and have the manifest point to hashes for
recognizers, candidates, and session state. Add a P4 regression test that creates
old manifest plus newer session state and proves restore refuses/falls back to
the valid prior generation.
## Drift
### D1 - P5a recall precision did not land
Category: drift
The L10 continuity spike design specified P5 as the T-experience gate, including
P5a recall precision@k stability. The landed contract explicitly records P5a as
`not_covered` (`evals/l10_continuity/contract.md:33-38`), while P5b/P5c landed
as anchor/coherence catastrophe checks (`evals/l10_continuity/predicates.py:291-380`).
This is honest in the contract, but it is still a design-vs-implementation cut.
Fix before next phase:
Do not claim T-experience or long-horizon recall stability from #567-#573 alone.
Add the held-out exact-CGA recall probe set and gate P5a before using the lived
spine as evidence of continuous experiencing memory.
### D2 - EngineIdentity scope differs from the L10 scoping recommendation
Category: drift
The L10 scoping brief recommended an EngineIdentity hash over
`schema_version + canonical_bytes` and a lineage chain recording schema migration.
The shipped EngineIdentity is instead a content hash over ratified identity,
safety, ethics, register, anchor-lens packs plus code revision
(`core/engine_identity.py:1-20`, `core/engine_identity.py:85-127`).
The manifest stamps this identity and its parent (`engine_state/__init__.py:161-189`).
This shipped model is internally coherent and tested, but it is not the same
identity primitive described in the scoping record. Lived experience remains in
Shape B+ `session_state.json`; the identity hash names the ratified substrate.
Fix before next phase:
Amend the L10/L11 design record or ADR scope to say EngineIdentity deliberately
means ratified substrate identity, not checkpoint-content identity. If checkpoint
content is still desired, add a separate `checkpoint_identity`/digest and link it
from the manifest rather than overloading EngineIdentity.
## Gaps
### G1 - P4 is sampled at the manifest/orphan boundary, not arbitrary interruption
Category: gap
The design brief elevated power-loss-at-arbitrary-instruction as a primary driver.
The landed runner simulates an orphan `.manifest.json.*.tmp` and the tests patch
`os.replace` for manifest writes (`evals/l10_continuity/runner.py:107-119`,
`tests/test_l10_continuity.py:204-238`). That is useful, but it does not enumerate
or sample every write boundary in the multi-file checkpoint.
Fix before next phase:
After H1 is fixed, extend P4 to cover crashes after each durable file in the
checkpoint sequence: recognizers, candidates, session state, and manifest.
### G2 - Idle learning backpressure remains unresolved
Category: gap
The roadmap/scoping notes called out absent-reviewer saturation as a real operator
condition. The proposal path has a pending cap (`DEFAULT_PENDING_CAP = 256`) and
emits a queue-full report when saturated (`teaching/proposals.py:48`,
`teaching/proposals.py:551-586`). `idle_tick()` treats proposal refusals as
non-created proposals and continues (`chat/runtime.py:858-865`). This is safe for
wrong=0, but it means learning can stall silently from the runtime result shape:
`IdleTickResult` has no refused/capacity count (`chat/runtime.py:508-514`).
Fix before next phase:
Expose refusal counts/reasons in `IdleTickResult` or idle telemetry so operators
can distinguish "no determined candidates" from "HITL queue full."
## Solid
### S1 - `idle_tick()` does not bypass HITL
Category: solid
`idle_tick()` contemplates pending candidates, calls `propose_from_candidate()`,
and never calls `accept_proposal()` or writes to the corpus (`chat/runtime.py:815-873`).
`propose_from_candidate()` first builds a proposal through `check_eligibility()`,
which rejects `polarity="undetermined"` (`teaching/proposals.py:165-181`), then
records a `created` event and replay evidence; acceptance remains a separate
operator path (`teaching/proposals.py:623-676`). Tests cover undetermined refusal
and determined-but-pending behavior (`tests/test_continuous_learning_idle.py:72-99`).
Answer to the audit question: an undetermined candidate cannot reach `pending`
through `idle_tick()`. A determined candidate can reach `pending`, but not
`accepted`; that is the intended HITL boundary.
### S2 - Vault restore does not reproject
Category: solid
`VaultStore.from_dict()` decodes persisted arrays, rebuilds `_exact_index`, leaves
`_matrix_cache` derived, and never calls `store()` or `reproject()`
(`vault/store.py:444-469`). The bright-line test asserts byte-identical restored
versors and exact recall (`tests/test_vaultstore_codec.py:41-84`). The existing
live `store()` path still auto-reprojects every N stores, but the restore path is
pure deserialize plus derived-index rebuild.
Answer to the audit question: engine-state restore does not reproject a vault.
### S3 - Persistence adds no forbidden field repair
Category: solid
The array codec is raw dtype/shape/base64 bytes; `FieldState.from_dict()` rebuilds
from decoded bytes via the constructor, which copies and validates shape/dtype but
does not normalize (`field/state.py:117-132`, `field/state.py:173-183`).
`SessionContext.restore()` composes component restore functions and explicitly
keeps vocab/persona outside session state (`session/context.py:373-405`).
No new persistence load path calls `unitize`, `normalize`, `null_project`, or a
field repair operator.
Answer to the audit question: no new persistence restore path normalizes or repairs
field state at a forbidden site.
### S4 - Schema-v2 compatibility/refusal is implemented
Category: solid
`_SCHEMA_VERSION = 2` is present (`engine_state/__init__.py:74`). `load_manifest()`
tolerates old/equal versions and refuses newer versions with
`IncompatibleEngineStateError` (`engine_state/__init__.py:191-209`). Tests cover v2
manifest write and v1 no-session-state fallback
(`tests/test_engine_state_session_persistence.py:40-54`).
### S5 - `persist_session_state` is default false and opt-in
Category: solid
`RuntimeConfig.persist_session_state` defaults to `False` with comments explaining
the per-turn O(n) snapshot cost and always-on-runtime scope (`core/config.py:279-285`).
`ChatRuntime` only loads/saves the lived session snapshot when the flag is true
(`chat/runtime.py:752-755`, `chat/runtime.py:789-795`). The L10 soak runner forces
the flag on for the resume lane (`evals/l10_continuity/runner.py:93-104`).
### S6 - Referent slot/history aliasing is preserved on restore
Category: solid
`ReferentRegistry.to_dict()` persists history as source of truth and slots as
slot-to-history-index so `from_dict()` reuses the same restored `ReferentEntry`
objects (`session/referents.py:173-204`). The test confirms the active slot object
is the same object as the corresponding history entry
(`tests/test_session_context_codec.py:52-63`).
### S7 - Source-tree invariant scans exclude `.claude/`
Category: solid
The whole-tree architectural scans filter `.claude` in both walk/rglob styles
(`tests/test_architectural_invariants.py:240-243`,
`tests/test_architectural_invariants.py:648-658`). This satisfies the audit note
that invariant source-tree scans must exclude agent worktrees.
## LOC Accounting
GitHub/merge-commit accounting for #567-#573:
| PR | Title | Files | Additions | Deletions | Net |
| --- | --- | ---: | ---: | ---: | ---: |
| #567 | L10 continuity spike | 8 | 1305 | 0 | +1305 |
| #568 | Shape B+ Phase A | 4 | 319 | 1 | +318 |
| #569 | Shape B+ Phase B | 2 | 138 | 0 | +138 |
| #570 | Shape B+ Phase C | 8 | 413 | 5 | +408 |
| #571 | Shape B+ Phase D+E | 9 | 215 | 40 | +175 |
| #572 | L11 identity continuity | 10 | 549 | 5 | +544 |
| #573 | Continuous learning idle | 2 | 203 | 0 | +203 |
| Total | | 43 | 3142 | 51 | +3091 |
This was not a net-removal stack. It added a new eval lane, persistence codecs,
state wiring, identity lineage, and idle-learning tests/substrate.
## Validation
Requested baseline:
```bash
core test --suite smoke -q
```
The bare `core` command was not initially on PATH in the fresh worktree. After
running through the local editable project entrypoint:
```bash
uv run --with-editable . core test --suite smoke -q
```
Result:
```text
87 passed in 116.32s (0:01:56)
```
No source code was changed during this audit. Only this report was added.