Add the Persona lane and the recency marker (Cut 6)

PersonaLane (crates/ghostlight/src/controllers.rs) is the unpersisted
Projector-then-Persona turn PA-Q3 A called for: no work-store row, no
persisted state, a restart just repeats the two inferences. Its `turn`
runs Projector then Persona for a NarrativePersona opportunity and
returns a receipt-bound PersonaTurn; `narrate` runs the Projector alone
over a Human opportunity for the player's own view. Both refuse the
wrong mode before touching the mailbox, and a retired subject is
refused by `select` itself (no live opportunity survives retirement).

The Projector-then-Persona prompt builders in `run_narrative` and
`run_projector` move into two private functions, `build_projector_invocation`
and `build_persona_invocation`, so PersonaLane and the narrative lane
build both prompts through the same one seam: a Persona's input is
Projector prose and nothing else (invariant 2).

PA-Q2's recency marker: `SubjectSnapshot.last_acted_at` is derived in
`snapshot` from one pass over `state.events` (the greatest
DecisionEvent.revision scoped to that subject), and `projector_knowledge`
stamps each row `"new": minted_at > last_acted_at`. A subject that never
acted sees every minted row as new; an unminted background fact (no
revision) is never new, having no arrival time to compare.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-22 07:38:06 +02:00
commit c4334cce3b
2 changed files with 1032 additions and 32 deletions

File diff suppressed because it is too large Load diff

View file

@ -83,7 +83,7 @@ pub use controllers::{
ControllerWorkLookup, ControllerWorkStore, ControllerWorkStoreError, ControllerWorkWrite,
GroupedCheckpoint, InferenceEvent, InferenceFault, InferenceOutput, InferencePort,
InferencePurpose, InferenceRequest, NarrativeCheckpoint, NarrativeRun, OperationalCheckpoint,
OperationalRun, PreparedInference, SubmissionDisposition, ToolResultOracle,
OperationalRun, PersonaLane, PreparedInference, SubmissionDisposition, ToolResultOracle,
TracingInferencePort, open_controller_work, open_inference,
};
pub(crate) use cover::{CellId, Constituent, Resolution};
@ -1031,6 +1031,11 @@ pub struct SubjectSnapshot {
/// The seed brief needs to say which subjects count; recomputing the
/// conjunction outside the kernel would be a second definition of alive.
pub qualified: bool,
/// The greatest `DecisionEvent.revision` whose scope is this subject, or
/// `None` if the subject never acted. Derived from `state.events` alone:
/// no new state, no schema change. The recency marker `projector_knowledge`
/// stamps onto each knowledge row is `minted_at > last_acted_at`.
pub(crate) last_acted_at: Option<u64>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@ -3738,6 +3743,20 @@ fn snapshot(state: &WorldState) -> Result<WorldSnapshot, KernelError> {
.map(move |fact| (fact, event.revision))
})
.collect();
// One pass over the causal record so each subject can be stamped with the
// revision of its own last exercised decision. `None` for a subject that
// never acted.
let last_acted_at: BTreeMap<SubjectId, u64> =
state
.events
.iter()
.fold(BTreeMap::new(), |mut acc, event| {
let entry = acc.entry(event.scope.subject_id).or_insert(event.revision);
if event.revision > *entry {
*entry = event.revision;
}
acc
});
let subjects = state
.subjects
.iter()
@ -3863,6 +3882,7 @@ fn snapshot(state: &WorldState) -> Result<WorldSnapshot, KernelError> {
})
.collect(),
qualified: qualifies(state, *subject_id),
last_acted_at: last_acted_at.get(subject_id).copied(),
})
})
.collect::<Result<Vec<_>, KernelError>>()?;