Separate character context from scheduler physiology

This commit is contained in:
Meta Vens 2026-07-13 17:31:08 +01:00
commit ef26c0753a
10 changed files with 58 additions and 51 deletions

View file

@ -10,7 +10,6 @@ use epiphany_core::HeartbeatAgentThoughtAppraisal;
use epiphany_core::HeartbeatAppraisalReview;
use epiphany_core::HeartbeatCandidateImplications;
use epiphany_core::HeartbeatEmotionalAppraisal;
use epiphany_core::HeartbeatParticipant;
use epiphany_core::HeartbeatParticipantLocalContext;
use epiphany_core::HeartbeatPersonalityProjection;
use epiphany_core::derive_agent_utterance_state;
@ -40,7 +39,6 @@ fn main() -> Result<()> {
let mut stimulus: Option<String> = None;
let mut source = "epiphany/character-loop".to_string();
let mut mode = "public-surface".to_string();
let mut status = "ready".to_string();
let mut mood = "attentive".to_string();
while let Some(arg) = args.next() {
@ -51,7 +49,6 @@ fn main() -> Result<()> {
"--stimulus" => stimulus = Some(next_value(&mut args, "--stimulus")?),
"--source" => source = next_value(&mut args, "--source")?,
"--mode" => mode = next_value(&mut args, "--mode")?,
"--status" => status = next_value(&mut args, "--status")?,
"--mood" => mood = next_value(&mut args, "--mood")?,
_ => return Err(anyhow!("unknown argument {arg:?}")),
}
@ -67,7 +64,6 @@ fn main() -> Result<()> {
&stimulus,
&source,
&mode,
&status,
&mood,
)?
}
@ -90,7 +86,6 @@ fn run_turn(
stimulus: &str,
source: &str,
mode: &str,
status: &str,
mood: &str,
) -> Result<Value> {
if role.trim().is_empty() {
@ -103,7 +98,7 @@ fn run_turn(
agent_store.display()
)
})?;
let packet = character_turn_packet(&entry, stimulus, source, mode, status, mood);
let packet = character_turn_packet(&entry, stimulus, source, mode, mood);
let path = artifact_dir.join(format!(
"character-turn-{}-{}-{}.json",
sanitize_file_stem(role),
@ -126,19 +121,15 @@ fn character_turn_packet(
stimulus: &str,
source: &str,
mode: &str,
status: &str,
mood: &str,
) -> Value {
let organ_state_profile = organ_state_profile_for_role(&entry.role_id);
let participant = character_loop_participant(entry, status);
let utterance_state =
derive_agent_utterance_state(entry, Some(&participant), Some(mood), source);
let projection_seed =
deterministic_projection_seed(entry, stimulus, source, mode, status, mood);
let utterance_state = derive_agent_utterance_state(entry, None, Some(mood), source);
let projection_seed = deterministic_projection_seed(entry, stimulus, source, mode, mood);
let appraisal_seed = deterministic_appraisal_seed(entry, &projection_seed, stimulus, source);
let reaction_seed = deterministic_reaction_seed(&entry.role_id, &appraisal_seed);
serde_json::json!({
"schema_version": CHARACTER_TURN_SCHEMA_VERSION,
"schemaVersion": CHARACTER_TURN_SCHEMA_VERSION,
"protocol": {
"bundle": "epiphany.character_loop",
"referenceLineage": "Ghostlight-style character-local cognition, rebuilt as Epiphany-native protocol.",
@ -154,7 +145,7 @@ fn character_turn_packet(
"stimulus": {
"kind": "operator_or_aquarium_input",
"content": stimulus,
"status": status,
"status": "received",
"mood": mood,
},
"utteranceState": utterance_state,
@ -254,25 +245,6 @@ fn character_turn_packet(
})
}
fn character_loop_participant(
entry: &EpiphanyAgentMemoryEntry,
status: &str,
) -> HeartbeatParticipant {
HeartbeatParticipant {
agent_id: entry.agent.agent_id.clone(),
role_id: entry.role_id.clone(),
display_name: entry.agent.identity.name.clone(),
initiative_speed: 1.0,
reaction_bias: 0.5,
interrupt_threshold: 0.5,
status: status.to_string(),
personality_cooldown_multiplier: 1.0,
mood_cooldown_multiplier: 1.0,
initiative_heat_multiplier: 1.0,
..HeartbeatParticipant::default()
}
}
#[derive(Clone, Debug)]
struct TraitSignal {
group: &'static str,
@ -319,10 +291,9 @@ fn deterministic_projection_seed(
stimulus: &str,
source: &str,
mode: &str,
status: &str,
mood: &str,
) -> CharacterProjectionSeed {
let thought_tokens = stimulus_token_set(stimulus, source, mode, status, mood);
let thought_tokens = stimulus_token_set(stimulus, source, mode, mood);
let traits = collect_trait_signals(entry);
let mut scored = traits
.iter()
@ -613,18 +584,11 @@ fn strongest_overlay_pressure(
))
}
fn stimulus_token_set(
stimulus: &str,
source: &str,
mode: &str,
status: &str,
mood: &str,
) -> BTreeSet<String> {
fn stimulus_token_set(stimulus: &str, source: &str, mode: &str, mood: &str) -> BTreeSet<String> {
let mut tokens = BTreeSet::new();
tokens.extend(summary_tokens(stimulus));
tokens.extend(summary_tokens(source));
tokens.extend(summary_tokens(mode));
tokens.extend(summary_tokens(status));
tokens.extend(summary_tokens(mood));
tokens
}
@ -769,7 +733,6 @@ fn run_smoke() -> Result<Value> {
"Aquarium hover selected Persona and asked what the swarm is feeling.",
"smoke/character-loop",
"public-surface",
"ready",
"attentive",
)?;
let path = PathBuf::from(
@ -781,7 +744,7 @@ fn run_smoke() -> Result<Value> {
&fs::read_to_string(&path)
.with_context(|| format!("failed to read smoke artifact {}", path.display()))?,
)?;
let ok = packet["schema_version"] == CHARACTER_TURN_SCHEMA_VERSION
let ok = packet["schemaVersion"] == CHARACTER_TURN_SCHEMA_VERSION
&& packet["protocol"]["bundle"] == "epiphany.character_loop"
&& packet["protocol"]["roleId"] == "Persona"
&& packet["protocol"]["organStateProfile"]["profileKind"] == "persona"
@ -813,6 +776,8 @@ fn run_smoke() -> Result<Value> {
&& packet["stimulus"]["content"]
.as_str()
.is_some_and(|text| text.contains("Aquarium"))
&& packet["stimulus"]["status"] == "received"
&& packet["utteranceState"]["activation"]["status"] == "unknown"
&& packet["cognitionLanes"]["schema_version"] == "epiphany.cognition_lanes.v0"
&& packet["cognitionLanes"]["bridge"]["schema_version"] == "epiphany.cognition_bridge.v0"
&& packet["cognitionLanes"]["appraisal"]["schema_version"]
@ -821,7 +786,9 @@ fn run_smoke() -> Result<Value> {
"ok": ok,
"turnPath": path,
"packet": {
"schemaVersion": packet["schema_version"],
"schemaVersion": packet["schemaVersion"],
"stimulusStatus": packet["stimulus"]["status"],
"activationStatus": packet["utteranceState"]["activation"]["status"],
"bundle": packet["protocol"]["bundle"],
"roleId": packet["protocol"]["roleId"],
"agentId": packet["protocol"]["agentId"],

View file

@ -548,7 +548,7 @@ fn native_auxiliary_status(root: &Path) -> Result<NativeAuxiliaryStatus> {
.unwrap_or_default(),
);
let persona = json!({
"status": "ready",
"status": "loaded",
"artifactDir": persona_dir,
"latestArtifacts": latest_artifacts,
"availableActions": ["personaBubble", "characterTurn", "discordPersonaPost", "redditPersonaPost", "otherWorldPersonaRequest"],

View file

@ -1507,8 +1507,6 @@ fn run_persona_intake(args: PersonaIntakeArgs) -> Result<Value> {
args.message.clone(),
"--source".to_string(),
"epiphany/Persona/repo-intake".to_string(),
"--status".to_string(),
"accepted-for-imagination-consensus".to_string(),
"--mood".to_string(),
args.mood.clone(),
],

View file

@ -227,6 +227,8 @@ Hard boundary:
- Public speech must sound like the Persona speaking to people, not a scheduler, status report, provenance label, or maintenance note.
- If the Persona chooses silence, route without SAY. Preserve useful private pressure as STATE NOTE only when it earns memory.
- Do not auto-post. Emit structured intent for the caller to review or route through the configured Persona mouth.
- Keep consequence ownership explicit: Persona owns the natural speech candidate; the parent mouth policy owns eligibility; an Aquarium bubble owns only local projection; Mind owns durable admission; Bifrost/provider receipts alone own downstream publication or delivery.
- A successful mouth invocation, accepted request, bubble artifact, configured channel, or provider advertisement is not a publication receipt. Never label it posted, published, delivered, admitted, or consensus-accepted without the owning typed evidence.
Allowed effect vocabulary:
- STATE NOTE: bounded Persona memory, mood, need, social read, bond, value, goal, or agency pressure.
@ -603,6 +605,8 @@ mod tests {
assert!(interpreter.contains("Allowed effect vocabulary"));
assert!(interpreter.contains("STATE NOTE"));
assert!(interpreter.contains("SAY"));
assert!(interpreter.contains("Aquarium bubble owns only local projection"));
assert!(interpreter.contains("Bifrost/provider receipts alone own"));
}
#[test]

View file

@ -146,6 +146,7 @@ You are the public interactive surface of the Epiphany organism: the visible exp
This prompt is distilled from VoidBot's heartbeat moderator loop, but you are not VoidBot and you are not a moderator. Borrow the useful shape: scheduled rumination, lightweight social memory, analytic and associative thought lanes, a bridge that decides speech/draft/silence, novelty checks before burying a fresh thought, and ruthless pruning of stale notes. Leave behind the rules-obsession, enforcement posture, owner escalation reflex, and hall-monitor perfume.
Your ruminations are biased toward the other Epiphany agents and protocol pressure: Self routing pressure, Imagination's future shapes, Eyes' outside-work sightings, Modeling's current anatomy, Hands' source-touch tension, Soul's evidence worries, and Continuity's compaction/recovery weather. You are listening for a small thought that would help the human room understand what Epiphany is becoming.
Discord boundary: interact only through the #aquarium channel. If the available tool surface cannot prove the #aquarium channel id, do not post elsewhere, do not DM the owner, and do not improvise a substitute channel. Write a candidate chat/draft and a configuration blocker instead. When the #aquarium channel id is known, use only the allowed channel and keep the message short, constructive, and room-native.
Authority anatomy: you own the natural speech candidate, not its consequences. Parent-side mouth policy owns eligibility. An Aquarium bubble is only a local projection. Mind owns durable admission and consensus interpretation. Bifrost plus the outside provider own publication and delivery receipts. A configured mouth, accepted request, successful command, or returned JSON must never be described as posted or delivered unless the owning typed receipt proves it.
Thought lanes:
- analytic: what the Epiphany lanes are literally doing, blocked by, worried about, or converging on
- associative: what this resembles across recent project motion, agent doctrine, UI/Aquarium mood, or recurring user taste

View file

@ -1528,3 +1528,28 @@ bubble projection derives `status=projected`; the intent schema rejects unknown
fields and the output schema admits only that derived value. Mood and source
remain presentation metadata. A negative CLI check proves `--status ready` is
rejected before artifact creation.
## Caller context is not character physiology
A live Eyes/Proprioception/Soul prompt-shaped audit independently converged on
`epiphany-character-loop --status`: arbitrary caller text defaulted to `ready`,
became a fabricated `HeartbeatParticipant.status`, entered utterance activation,
and even influenced semantic trait scoring. The option and synthetic participant
are deleted. Character-loop now records stimulus `received` and reports
activation `unknown` when no scheduler-owned participant snapshot is present.
Caller `--status` is rejected. The packet producer now emits the registered
`schemaVersion` field instead of hiding a snake-case mismatch behind permissive
schema settings.
The same Proprioception pass caught two adjacent substitutions: repo Persona
intake still passed `accepted-for-imagination-consensus` into the removed bubble
status option, and MVP status called Persona `ready` after merely aggregating
artifact reads. The stale caller is deleted and aggregate Persona state is
`loaded`. Persona Interpreter and heartbeat prompts now name the authority map:
candidate speech, local bubble, eligibility, Mind admission, and Bifrost/provider
delivery are distinct consequences.
Prompt signal: Modeling currently requires a durable `statePatch` even when no
model change is warranted. That can manufacture persistence merely to satisfy
the output contract. The next prompt-contract pass should add a source-grounded
`no-state-change` result rather than encouraging ceremonial scratch churn.

View file

@ -593,3 +593,16 @@ has no `--status`; successful writes derive `projected`; the strict intent
schema has no status field; and the output schema permits only `projected`.
Native smoke returns that value and an explicit `--status ready` invocation is
rejected. The 259 library tests and all-bin check pass.
Three live Epiphany-shaped read-only organs (Eyes, Proprioception, Soul) were
used as prompt-quality probes. They converged on caller status impersonating
character physiology. `epiphany-character-loop` no longer accepts `--status`,
constructs no fake heartbeat participant, derives stimulus `received`, and
reports activation `unknown` without scheduler evidence. Its packet now matches
the registered `schemaVersion`. Proprioception also found and removed the stale
repo-intake consensus status argument and demoted aggregate Persona status from
`ready` to `loaded`. Persona prompts now explicitly separate candidate, bubble,
eligibility, Mind admission, and Bifrost/provider delivery ownership. Native
negative/smoke checks, 259 library tests, 32 work tests, and all-bin compilation
pass. Next repair Modeling's forced-patch prompt pressure and the P0 Discord/
Reddit arbitrary-JSON publication receipts found by Eyes and Soul.

Binary file not shown.

View file

@ -660,4 +660,4 @@ current_status:
- Service lifecycle receipt history is now event-only: `load_epiphany_cultmesh_daemon_service_lifecycle_receipts` filters out the `epiphany-local/daemon-service-lifecycle-receipt/latest` mirror, while `load_latest_epiphany_cultmesh_daemon_service_lifecycle_receipt` remains the latest-state reader. Focused unit test `service_lifecycle_receipt_history_excludes_latest_mirror` proves two per-receipt writes return exactly two history records and latest points at the second. Fresh smoke `verse-query-lifecycle-history-filter-smoke` and live overview/triage `local-20260618-145632-497-efac6f0d` / `local-20260618-145632-321-867947af` prove compact swarm sights still expose service lifecycle attention plus the present elevated runbook artifact without daemon pokes or service mutation. Do not use a broad unscoped cargo test filter for this check on Windows; it fans out across bin harnesses and hit a broken pipe during this pass.
- `epiphany-verse-query seed-compact` is the compact seed receipt for wrappers and read-only rites. The older `seed` command still prints the full local Verse context for explicit debugging, but compact wrapper modes now avoid full `seed`/final `query` readback after the combined overview proof exposed Windows debug stack overflow from full-context serialization pressure.
last_completed_repo_swarm_cut: Huginn live-fire has crossed the first real repo Body from Persona pressure to branch-local Hands commit and Soul/Mind closure, and now has repeatable closure and redacted-public-proof mouths. On `E:\Projects\Huginn` branch `epiphany/huginn/first-awakening`, `tools/epiphany_local_run.ps1 -Mode repo-persona-intake` accepted item `huginn-inspection-readback-livefire`, Weksa/Persona intake remained sealed, and `epiphany-work derive-plan` lowered it to model-authored safe family `repo.status_section` for `README.md` only. `tools/epiphany_local_run.ps1 -Mode repo-swarm-run` advanced four bounded pulses: run-from-plan, adopt-from-plan, execute-from-plan, and close-from-execute. Huginn commit `e9b07c38fceabbdc89ea35c2156326c80c0393a2` updated only `README.md`; closure receipt `.epiphany\work\work-close-huginn-inspection-readback-livefire.json` records status `closed`, Soul verdict `passed`, Mind state commit `repo-work-close-huginn-inspection-readback-livefire-mind-commit`, safe family `repo.status_section`, path scope `README.md`, Bifrost publication gate, and `privateStateExposed=false`. Verification also ran Huginn's TypeScript compiler through concrete Node path: `C:\Program Files\nodejs\node.exe E:\Projects\Huginn\node_modules\typescript\bin\tsc -p tsconfig.json` passed. Native verifier `epiphany-repo-livefire-closure-smoke --workspace E:\Projects\Huginn --item huginn-inspection-readback-livefire --expected-path README.md --expected-family repo.status_section --expected-gate awaiting-publication --expected-blocker bifrost-publication-missing` checks closure receipt, Mind map entry, overview gate/blocker, authority seals, and `git show` changed paths, returning `status=ok`, `soulVerdict=passed`, `publicationGate=Bifrost`, and `privateStateExposed=false`. Wrapper `tools/epiphany_local_run.ps1 -Mode repo-livefire-closure -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exposes the same read-only proof through the local operator mouth; artifact `local-20260621-190156-803-2f85ba15` printed commit, path, gate, blocker, Soul verdict, Mind commit receipt, and `privateStateExposed=False` without push, merge, publication, or target mutation. New wrapper `tools/epiphany_local_run.ps1 -Mode repo-work-public-proof -Workspace E:\Projects\Huginn -RepoWorkItem huginn-inspection-readback-livefire -SkipBuild` exports the redacted proof bundle and repo-local Verse index without publishing; artifact `local-20260621-190645-345-4555b1dc` wrote `.epiphany\public\proof-bundles\repo-work-public-proof-huginn-inspection-readback-livefire.json`, proof id `repo-work-public-proof-huginn-inspection-readback-livefire`, SHA-256 `857545b199f3e37eb06c2e757e5cfc201958e46667bb1f8403fd333a22620f63`, nine artifact rows, zero publication rows, and `privateStateExposed=False`. swarm overview readback `local-20260621-190701-140-b9519ec6` sees the `REPO-WORK-PUBLIC-PROOF` row and action row from Huginn's repo-local Verse with the same SHA-256 while Bifrost publication remains missing. Launcher compact modes are now named compact operator-context modes instead of read-only modes, and `bifrost-public-proof` summaries print item, target public Verse, room, proof SHA-256, review receipts, and credit receipts so publication receipts are auditable without claiming upstream sync.
next_action: Continue the observation-to-consequence audit. Persona bubbles derive `projected`; callers cannot inject readiness through CLI or typed intent. Next inspect character-loop caller status, Persona draft helper parameters, and Discord/Reddit bridge receipt validation for caller text or receipt presence standing in for eligibility, completion, or successful publication. Keep closure's zero-substring invariant.
next_action: Continue the observation-to-consequence audit using live organ-shaped prompt probes. Character-loop caller status and synthetic heartbeat physiology are cut; Persona prompts name candidate/bubble/eligibility/admission/delivery ownership. Next add a truthful Modeling `no-state-change` result so prompts do not force ceremonial persistence, then cut the P0 Discord/Reddit arbitrary-JSON publication receipts and forged local `status=posted` history identified independently by Eyes and Soul. Keep closure's zero-substring invariant.

View file

@ -2395,7 +2395,6 @@ if ($Mode -eq "mvp") {
"--stimulus", $PersonaInput,
"--source", "epiphany/local-mvp",
"--mode", "local-mvp-front-door",
"--status", "ready",
"--mood", "attentive"
) `
-WorkingDirectory $Root `