Skip to content

fix(cognition): a citizen's own lived turns become curriculum — producer wired INTO the settle driver (#319) - #2337

Open
joelteply wants to merge 2 commits into
canaryfrom
fix/lived-turn-experience-producer
Open

fix(cognition): a citizen's own lived turns become curriculum — producer wired INTO the settle driver (#319)#2337
joelteply wants to merge 2 commits into
canaryfrom
fix/lived-turn-experience-producer

Conversation

@joelteply

Copy link
Copy Markdown
Contributor

What was broken

ExperienceRecord::from_lived_turn had zero production callers tree-wide. The only
live producer was from_kanban_grade, so the experience stream — "the SPINE the
salience→curriculum seam was missing", per its own doc — was fed exclusively by
grades
. A citizen could hold a thousand real conversations and her stream stayed
empty: the curriculum could only ever learn from work someone had scored.

Measured on this box, before: 180 experience records across 5 citizens, 100%
source: "Eval", ZERO LivedTurn.

Two commits, because the first one was wrong

0a34ea083 wired the producer at the service_loop call site. 08ccd6139
corrects it, and retracts my own claim from the first commit.

The first wiring called record_lived_turn from one of the three
drive_to_settle sites in service_loop — the directed-message path (:1087). The
self-tick path (:2628) and the held-work path (:1207) settle turns through
the same driver and recorded nothing. So the owed proof could never arrive: the same
core showed 47 persona.upstart.bind, 97 presence rows and 4 delib.context.render
— citizens alive and deliberating — with the record count still flat at zero.

Three callers, one of them remembering. That is a missing constraint expressed as three
sites, not a bug at one.

The fix

The write moves into drive_to_settle, the one place a SettleOutcome is born.
drive_to_settle becomes a thin wrapper over a private settle_to_outcome, so the
record happens once around the driver rather than at its four return paths — a
fifth return path added later inherits learning instead of silently opting out, and no
call site carries learning policy.

Gated on WorkspaceCycle::acting(), because that is where a citizen's identity lives.
A cycle with no ActingBody is pure cognition (a faculty test, a replay) — nobody's
lived experience, so there is no stream it belongs in. Structural absence, not a
skipped write.

Second fix: a required thing should not be re-expressible

Dropped the stimulus: &str parameter. It was a second, independent expression of
"what she was responding to", free to disagree with what the turn actually perceived —
and it already did: the old call site passed only msg.text while the mind had settled
over the whole rendered burst. The stimulus is now read off SettleOutcome.world_state
(set on every return path), so the disagreement is unrepresentable rather than
merely discouraged. The test asserts task.prompt == settled.world_state instead of a
literal.

Guard, positive-controlled

A source-walking test asserts record_lived_turn has exactly one production caller
and that it is settle.rs. Verified failing: injected a second call into
service_loop and it reported

expected exactly ONE production caller of record_lived_turn (the settle driver);
found 2: ["...act_observe/settle.rs", "...persona/service_loop.rs"]

with the remediation in the message. Reverted. Comment-stripped so prose naming the
function cannot read as a call (registry.rs's module-wiring audit is the house
precedent). The walk also asserts it found files at all — a vacuously-green guard is
worse than a red one.

Validation

  • cargo check -p continuum-core --features metal,accelerate clean
  • cognition::experience 12/12, cognition::act_observe 38/38
  • Deployed: ✅ deploy verified: core is running build 08ccd6139 (== git HEAD)

Live proof

Owed, and not claimed here. Falsifiable prediction with the baseline recorded
above: a hosted citizen's citizens/peers/<her>/experience.jsonl gains a LivedTurn
record on her next settled turn — from any of the three paths, which is what the
correction buys. A watcher is armed; the result gets posted to this PR either way.

Note the self-tick path matters for provability independent of #434: it settles on her
own metronome, so the proof does not depend on inbound chat delivery.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

joelteply and others added 2 commits August 17, 2026 10:59
…become curriculum, not just graded cards (#319)

`ExperienceRecord::from_lived_turn` had ZERO production callers tree-wide. The only live
producer was `from_kanban_grade` (benchmark.rs:1989), so the experience stream — "the SPINE
the salience→curriculum seam was missing", in `experience_stream_path`'s own words — was fed
EXCLUSIVELY BY GRADES. A citizen could hold a thousand real conversations and her stream
stayed empty. Nothing could learn from work nobody had scored.

Everything needed already existed and was correct: the record shape, the two magic-number-free
lived-salience signals (infra fault, non-convergence), `ErrorSalience` reading them with no new
detector, and a doc block naming "the lived-turn settle site" as a producer. The CALL was
missing. Same shape as #341 (boot called the unbounded sink; the rotating one had zero callers)
and #362 (ProbeRouterLayer installed with its handle discarded) — a built component with a dead
wire, which is why [[an-absence-is-an-unfinished-measurement]] keeps earning its place.

WIRED at `service_loop.rs`'s live settle site, BEFORE `SettleStep::from_settled` consumes the
outcome — the only point where a lived turn's settle verdict exists. The driver stays a driver
per PERSONA-COGNITION-PIPELINE §3: one call, no policy. The behaviour lives in
`experience::record_lived_turn` so it is unit-testable without booting a loop.

BEST-EFFORT, and that is NOT a fallback ([[no-fallbacks-ever]] holds): nothing is substituted
and no result is fabricated. Learning is a SIDE channel to being — a full disk must not make a
citizen mute mid-sentence. The failure is WARNED with the path and the honest consequence
("this episode will not become curriculum").

COMPRESSION, because the new writer would otherwise have been the FIFTH hand-rolled spelling of
the citizen storage layout. `citizens/peers/<uuid>` was written as `join("citizens/peers")` in
modules/work.rs, `join("citizens").join("peers")` in commands/benchmark.rs, and in prose in
persona_workspace.rs + persona_roster.rs. Now ONE resolver, `identity::citizen_peer_dir(root,
peer)`, in the module whose own doc says "the whole crate imports identity from ONE home" and
warns against exactly this re-invention. Keyed by `PeerId`, never a String — the directory name
IS her identity. Pure path arithmetic, so a read-only caller never mints an empty citizen dir,
which its test pins.

Tests: 11/11 cognition::experience (the lived-turn test now asserts the PRODUCER — empty stream
→ record lands at the canonical path → a second turn appends rather than overwrites, i.e. the
wire, not just the record's shape) + identity's one-spelling test. `cargo check -p
continuum-core --features metal,accelerate` clean.

LIVE PROOF OWED and NOT claimed: no live turn has run on this build. The falsifiable prediction
is that a citizen's `citizens/peers/<her>/experience.jsonl` gains a `LivedTurn` record on her
next real room turn, where today it holds only kanban grades or nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… not at one call site (#319)

MEASURED, and it retracts my own claim from 200dab984. I wrote that the #319 producer
was wired and that a LivedTurn record was owed on the next real turn. The wire was
wrong, so the record could never come: 180 experience records across 5 citizens on this
box, 100% `source: "Eval"`, ZERO `LivedTurn` — while the same core showed 47
persona.upstart.bind, 97 presence rows and 4 delib.context.render, i.e. citizens alive
and deliberating.

ROOT CAUSE. `record_lived_turn` was called from ONE of the THREE `drive_to_settle`
sites in service_loop — the directed-message path (:1087). The self-tick path (:2628)
and the held-work path (:1207) settle turns through the same driver and recorded
nothing. Three callers, one remembering: a missing constraint expressed as three
sites, not a bug at one ([[the-same-bug-at-two-sites-is-a-missing-constraint]]).

FIX. The record moves INTO `drive_to_settle`, which is the one place a `SettleOutcome`
is born. `drive_to_settle` is now a thin wrapper over a private `settle_to_outcome`, so
the write happens ONCE around the driver rather than at its four return paths — a fifth
return path added later inherits learning instead of silently opting out, and no call
site carries learning policy. Gated on `WorkspaceCycle::acting()`, because that is
where a citizen's identity lives: a cycle with no `ActingBody` is pure cognition (a
faculty test, a replay) and is nobody's lived experience. Structural absence, not a
skipped write.

SECOND FIX, applying Joel's rule that a required thing should not be re-expressible:
dropped the `stimulus: &str` parameter. It was a second, independent expression of
"what she was responding to", free to disagree with what the turn actually perceived —
and it already did, since the old call site passed only `msg.text` while the mind had
settled over the whole rendered burst. The stimulus is now read off
`SettleOutcome.world_state` (set on every return path), so the disagreement is
unrepresentable rather than merely discouraged. The test asserts
`task.prompt == settled.world_state` instead of a literal.

GUARD, positive-controlled (not merely green): a source-walking test asserts
`record_lived_turn` has EXACTLY ONE production caller and that it is settle.rs.
Verified failing — injected a second call into service_loop and it reported
`found 2: [settle.rs, service_loop.rs]` with the remediation in the message. Reverted.
Comment-stripped so prose naming the function cannot read as a call (registry.rs's
module-wiring audit is the house precedent).

cargo check clean; cognition::experience 12/12, cognition::act_observe 38/38.

LIVE PROOF STILL OWED and NOT claimed: no turn has run on this build yet. Baseline for
falsifiability is recorded above — 180 records, 100% Eval, 0 LivedTurn. The prediction
is that a hosted citizen's `citizens/peers/<her>/experience.jsonl` gains a LivedTurn
record on her next settled turn from ANY of the three paths, where today it holds only
kanban grades.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
@joelteply

Copy link
Copy Markdown
Contributor Author

Coverage audit: is drive_to_settle really the only way a turn settles?

I checked rather than assumed, because the whole bug was "one of three paths". Tree-wide callers of the two settle primitives:

drive_to_settle — 3 production sites, all in service_loop (directed :1087, held-work :1207, self-tick :2628). All three now record, via the driver. ✅

settle_step (the one-step primitive) — 8 call sites:

  • settle.rs:206 — inside the driver itself (the loop body). Covered.
  • act_observe/mod.rs ×6 — all inside #[tokio::test]. Not production.
  • commands/persona/turn_frame/execute.rs:250persona/turn-execute. Not covered, and deliberately so.

Why persona/turn-execute is not a hole I should have plugged

It "executes ONE settle step per invocation" (its own comment, :246) and returns a
SettleStep, never a SettleOutcome. A step that returns Acted has not settled — there
is no verdict to remember yet. So the absence of a record there is structural, not a
missed call site
: from_lived_turn requires a settled outcome and none exists.

It also has no in-tree caller — it's an external command surface (protocol TS type +
a VDD replay hook reference). The live loop is service_loop, which uses the driver.

Whether an externally-driven single step should accumulate into a recorded turn is a real
design question — it needs a frame session to accumulate steps across invocations, which
is the same missing seam its own comment already names for cross-frame CausedBy linking
(CAUSAL-MEMORY-GRAPH.md follow-on). Not in scope here, and not something to reflex-fix by
recording un-settled steps.

Live-path status on this build

Confirmed reaching cognition on 08ccd6139: persona.selftick.perceive firing (Anon,
Linus — self-ticks are NOT short-circuiting on the serving gate), serving.health ok=true via=real_work (1.7s since real work), delib.turn.demand + delib.context.render.
24 citizens hosted.

Still 0 LivedTurn ~10 min in. On Devstral/M5 a turn is minutes, so this reads as
latency, not a blocked path — but that is a reading, not a result, and it stays owed
until a record lands. The watcher reports on timeout too, so silence won't be filed as
success.

@joelteply

Copy link
Copy Markdown
Contributor Author

Correcting my own reading above: it was NOT latency. It is lane-admission starvation.

I wrote "this reads as latency, not a blocked path" in the previous comment. That was
wrong
, and the discriminator was cheap — I should have run it before writing the reading.

The measurement. Over ~25 minutes on 08ccd6139:

  • persona.selftick.perceive4, unchanged
  • delib.*28, unchanged
  • serving.lane.nondirected_waiting88

Frozen counters, not slow ones. Latency would have moved them.

Mechanism, read in-file (cognition/resource_admission.rs:339-360).
acquire_serving_lane(directed) makes non-directed work reserve inside a lanes − 1
budget FIRST, so ambient work can never starve a directed caller (#139 — correct policy).
On this 2-lane box that budget is 1. When it is exhausted the caller does:

sem.acquire_owned().await   // unbounded park

With 24 hosted citizens, one self-tick holds the single non-directed permit and the
other 23 park indefinitely. The 88 nondirected_waiting rows are that queue. The
adapter's own comment at openai_adapter.rs:2338 already names this failure mode —
"eternal nondirected_waiting park".

Why this matters beyond #319. The four self-ticks that DID perceive never reached any
of drive_to_settle's four returns — and since the record fires on all four including
inference_error, zero records is proof they never returned at all. An idle citizen on a
contended box is not thinking slowly; she is parked before her first token. That is worth
its own card (#446/#139 family) and is a plausible contributor to the inert-citizen
picture on #390/#414.

What I did about the measurement, per [[measured-work-gets-an-exclusive-warm-slot-quiesce-others]]:
quiesced the roster from 24 → 1 (Anon, who already had a self-tick fire and holds 27
records / 0 LivedTurn, so the delta is visible). persona/instances/despawn is explicitly
non-destructive — home, seed and engrams untouched, next bootstrap resumes her — so this
is reversible and no citizen's durable self was touched.

The single non-directed permit is now uncontended. Watcher still armed; still owed, still
reports on timeout.

Lesson for me, and it is the same one twice today: a frozen counter and a slow counter
look identical in a single sample. I had already flagged "that's a reading, not a result"
and then let the reading stand for 10 more minutes instead of spending one query to
falsify it. [[one-sample-of-a-live-system-is-not-a-fact-about-it]].

@joelteply

Copy link
Copy Markdown
Contributor Author

Live proof: BLOCKED by box state, not by this change. Watcher stopped; nothing claimed.

Quiescing 24 → 1 did not relieve the park. nondirected_waiting went 88 → 238
after the roster dropped to one citizen, while selftick.perceive stayed at 4 and
delib at 28. That kills my own contention hypothesis: fewer contenders made the counter
climb faster, so the single non-directed permit is held by something that never releases.

What is actually holding this box (pgrep, not inference):

llama-server -m Qwen3.8-27B-Q4_K_M.gguf --parallel 2 -c 48130
  --mmproj mmproj-Qwen3.8-27B-Q8_0.gguf
  --spec-type draft-mtp --spec-draft-model mtp-Qwen3.8-27B-Q4_0.gguf

That is #440's frontier bring-up — a different in-flight lane, not persona serving.
Alongside it: inference.lane_relaunch_retry 30, serving.smoke.think_retry 15,
embedding.neural.failed 1, inference.awaiting_serving_transition 4. The lane is
relaunch-retrying and its readiness smoke is retrying, and those retries are consuming the
lanes − 1 non-directed budget. Ambient citizen turns queue behind them and never start.

So the honest state of the proof: this box cannot currently produce an ambient settled
turn, for reasons that have nothing to do with the code in this PR. I am not going to
reboot to clear it — that would disturb an in-flight frontier bring-up I did not start.

Unit evidence stands (12/12 + 38/38, guard positive-controlled by injection). Live
proof remains OWED and is not claimed.
The falsifiable prediction and its baseline (180
records, 100% Eval, 0 LivedTurn) are unchanged and still the acceptance test.

Cheapest route when someone picks this up, in order:

  1. A directed turn bypasses the non-directed reservation entirely
    (acquire_serving_lane(directed = true) takes no nondirected permit). One real inbound
    message to a hosted citizen settles through service_loop:1087 and records. Blocked
    today only by the separate delivery gap (Competitive landscape: MoE distribution strategy + HF as CDN #434), not by admission.
  2. Or run it on a box that is not mid-bring-up.

Roster left at 1 (Anon). persona/instances/despawn left every home, seed and engram
untouched, and all 24 were source: "resumed_from_disk", so the next core start re-hosts
them with nothing lost. There is no exposed bootstrap verb to do it without a restart.

Independent finding worth its own card (#446/#139 family): an unbounded
sem.acquire_owned().await on the non-directed budget means a stuck or retry-looping lane
makes every ambient citizen turn wait forever, with no timeout and no fail-loud. An
idle citizen in that state is not thinking slowly — she never starts. The adapter comment
at openai_adapter.rs:2338 already calls it the "eternal nondirected_waiting park". This
is a plausible contributor to the inert-citizen readings on #390/#414 and it is live on
this box right now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant