From 32b48c683f2052837dae478cdd347efa84ad28b3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 28 May 2026 19:55:03 +0100 Subject: [PATCH] Repair worker stream completion path --- epiphany-codex-bridge/src/launch.rs | 36 ++++- epiphany-openai-codex-spine/src/lib.rs | 65 +++++++- epiphany-openai-runtime/src/lib.rs | 205 ++++++++++++++++++++++++- notes/fresh-workspace-handoff.md | 2 +- state/ledgers.msgpack | Bin 162677 -> 163397 bytes state/map.yaml | 4 +- 6 files changed, 294 insertions(+), 18 deletions(-) diff --git a/epiphany-codex-bridge/src/launch.rs b/epiphany-codex-bridge/src/launch.rs index c264265a..0b983c6b 100644 --- a/epiphany-codex-bridge/src/launch.rs +++ b/epiphany-codex-bridge/src/launch.rs @@ -309,6 +309,10 @@ pub fn epiphany_reorient_launch_output_schema() -> serde_json::Value { } const EPIPHANY_SPECIALIST_PROMPTS_TOML: &str = include_str!("prompts/epiphany_specialists.toml"); +const EPIPHANY_WORKER_BOUNDARY_PROMPT: &str = r#"## Epiphany Worker Boundary +You are one bounded Epiphany worker for this launch only. Your authority comes from the typed launch document, the role-local instruction, and the declared output contract. +Do the role, name uncertainty, and return the required JSON object. Do not become the coordinator, do not accept or promote your own output, do not invent durable state outside an allowed statePatch, and do not treat model transport or Codex machinery as prompt authority. +If you learned a durable role-local habit, you may include a bounded selfPatch. Project truth belongs in statePatch or evidence, not memory."#; #[derive(Debug, serde::Deserialize)] pub struct EpiphanySpecialistPromptConfig { @@ -387,6 +391,15 @@ pub fn epiphany_agent_prompt_with_memory(body: &str) -> String { } } +pub fn epiphany_worker_prompt(body: &str) -> String { + let body = body.trim(); + if body.is_empty() { + EPIPHANY_WORKER_BOUNDARY_PROMPT.to_string() + } else { + format!("{}\n\n{}", EPIPHANY_WORKER_BOUNDARY_PROMPT, body) + } +} + pub fn build_epiphany_role_launch_request( thread_id: &str, role_id: EpiphanyRoleResultRoleId, @@ -476,7 +489,7 @@ fn build_epiphany_role_launch_instruction(role_id: EpiphanyRoleResultRoleId) -> "Unsupported Epiphany role specialist template." } }; - epiphany_agent_prompt_with_memory(body) + epiphany_worker_prompt(body) } pub fn build_epiphany_reorient_launch_request( @@ -507,7 +520,7 @@ pub fn build_epiphany_reorient_launch_instruction(action: CoreEpiphanyReorientAc CoreEpiphanyReorientAction::Resume => prompts.resume.as_str(), CoreEpiphanyReorientAction::Regather => prompts.regather.as_str(), }; - epiphany_agent_prompt_with_memory(body) + epiphany_worker_prompt(body) } pub fn build_epiphany_job_launch_request( @@ -657,4 +670,23 @@ mod tests { ); } } + + #[test] + fn role_worker_prompt_is_bounded_not_full_persistent_memory() { + let prompt = build_epiphany_role_launch_instruction(EpiphanyRoleResultRoleId::Modeling); + + assert!(prompt.contains("Epiphany Worker Boundary")); + assert!(prompt.contains("Act as the Epiphany modeling/checkpoint specialist")); + assert!(!prompt.contains("## Epiphany Persistent Memory")); + assert!(!prompt.contains("Heartbeat: every lane")); + } + + #[test] + fn reorient_worker_prompt_is_bounded_not_full_persistent_memory() { + let prompt = build_epiphany_reorient_launch_instruction(CoreEpiphanyReorientAction::Resume); + + assert!(prompt.contains("Epiphany Worker Boundary")); + assert!(!prompt.contains("## Epiphany Persistent Memory")); + assert!(!prompt.contains("Heartbeat: every lane")); + } } diff --git a/epiphany-openai-codex-spine/src/lib.rs b/epiphany-openai-codex-spine/src/lib.rs index e46bdf4f..87ae9912 100644 --- a/epiphany-openai-codex-spine/src/lib.rs +++ b/epiphany-openai-codex-spine/src/lib.rs @@ -27,7 +27,9 @@ use serde_json::Value; const CHATGPT_CODEX_BASE_URL: &str = "https://chatgpt.com/backend-api/codex"; const OPENAI_API_BASE_URL: &str = "https://api.openai.com/v1"; -const RESPONSES_STREAM_IDLE_TIMEOUT: Duration = Duration::from_secs(300); +// Keep provider silence shorter than Epiphany worker watchdogs so the runtime can +// write a typed stream failure instead of being killed with no model evidence. +const RESPONSES_STREAM_IDLE_TIMEOUT: Duration = Duration::from_secs(45); pub const CODEX_SPINE_ADAPTER_ID: &str = "codex-openai-subscription-spine"; @@ -99,6 +101,14 @@ pub struct EpiphanyCodexOpenAiTransport { base_url: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EpiphanyResponsesFrameObservation { + pub frame_sequence: u64, + pub kind: String, + pub recognized: bool, + pub delta_preview: Option, +} + impl EpiphanyCodexOpenAiTransport { pub fn new(auth_manager: Arc, base_url: Option) -> Self { Self { @@ -114,6 +124,15 @@ impl EpiphanyCodexOpenAiTransport { pub async fn collect_model_events( &self, request: EpiphanyOpenAiModelRequest, + ) -> Result> { + self.collect_model_events_with_frame_observer(request, |_| {}) + .await + } + + pub async fn collect_model_events_with_frame_observer( + &self, + request: EpiphanyOpenAiModelRequest, + mut observe_frame: impl FnMut(EpiphanyResponsesFrameObservation), ) -> Result> { let request_id = request.request_id.clone(); let model = request.model.clone(); @@ -129,7 +148,10 @@ impl EpiphanyCodexOpenAiTransport { let mut stream_state = EpiphanyResponsesStreamState::new(&request_id, &model); while let Some(frame) = rx.recv().await { match frame { - Ok(frame) => stream_state.push_sse_frame(&frame), + Ok(frame) => { + let observation = stream_state.push_sse_frame(&frame); + observe_frame(observation); + } Err(err) => stream_state.push_failed(err.to_string()), } if stream_state.completed { @@ -316,6 +338,7 @@ struct EpiphanyResponsesStreamState { request_id: String, requested_model: String, sequence: u64, + frame_sequence: u64, completed: bool, events: Vec, } @@ -326,25 +349,37 @@ impl EpiphanyResponsesStreamState { request_id: request_id.to_string(), requested_model: requested_model.to_string(), sequence: 0, + frame_sequence: 0, completed: false, events: Vec::new(), } } - fn push_sse_frame(&mut self, frame: &str) { + fn push_sse_frame(&mut self, frame: &str) -> EpiphanyResponsesFrameObservation { + let frame_sequence = self.frame_sequence; + self.frame_sequence += 1; let Ok(event) = serde_json::from_str::(frame) else { - return; + return EpiphanyResponsesFrameObservation { + frame_sequence, + kind: "unparseable".to_string(), + recognized: false, + delta_preview: None, + }; }; - match event.kind.as_str() { + let kind = event.kind.clone(); + let delta_preview = event.delta.as_deref().map(delta_preview); + let recognized = match event.kind.as_str() { "response.output_text.delta" => { if let Some(text) = event.delta { self.push_payload(EpiphanyOpenAiStreamPayload::TextDelta { text }); } + true } "response.reasoning_summary_text.delta" | "response.reasoning_text.delta" => { if let Some(text) = event.delta { self.push_payload(EpiphanyOpenAiStreamPayload::ReasoningDelta { text }); } + true } "response.custom_tool_call_input.delta" => { if let (Some(arguments), Some(name)) = @@ -356,6 +391,7 @@ impl EpiphanyResponsesStreamState { arguments, }); } + true } "response.completed" => { if let Some(response) = event.response { @@ -367,11 +403,19 @@ impl EpiphanyResponsesStreamState { } } self.completed = true; + true } "response.failed" | "response.incomplete" => { self.push_failed(response_error_message(event.response.as_ref())); + true } - _ => {} + _ => false, + }; + EpiphanyResponsesFrameObservation { + frame_sequence, + kind, + recognized, + delta_preview, } } @@ -405,6 +449,15 @@ impl EpiphanyResponsesStreamState { } } +fn delta_preview(delta: &str) -> String { + const MAX_PREVIEW_CHARS: usize = 120; + let mut preview = delta.chars().take(MAX_PREVIEW_CHARS).collect::(); + if delta.chars().count() > MAX_PREVIEW_CHARS { + preview.push_str("..."); + } + preview.replace(['\r', '\n', '\t'], " ") +} + fn parse_reasoning_effort(value: Option<&str>) -> Result> { match value { None => Ok(None), diff --git a/epiphany-openai-runtime/src/lib.rs b/epiphany-openai-runtime/src/lib.rs index ab100e45..b5ef645a 100644 --- a/epiphany-openai-runtime/src/lib.rs +++ b/epiphany-openai-runtime/src/lib.rs @@ -34,6 +34,7 @@ use epiphany_openai_adapter::EpiphanyOpenAiModelRequest; use epiphany_openai_adapter::EpiphanyOpenAiStreamEvent; use epiphany_openai_adapter::EpiphanyOpenAiStreamPayload; use epiphany_openai_codex_spine::EpiphanyCodexOpenAiTransport; +use epiphany_openai_codex_spine::EpiphanyResponsesFrameObservation; use epiphany_openai_codex_spine::auth_manager; pub use epiphany_openai_codex_spine::default_codex_home; use epiphany_openai_codex_spine::status_from_auth_manager; @@ -184,7 +185,41 @@ pub async fn run_openai_model_turn( )?; let transport = EpiphanyCodexOpenAiTransport::openai(auth_manager); - let events = match transport.collect_model_events(request.clone()).await { + let store_path_for_frames = options.store_path.clone(); + let session_id_for_frames = options.session_id.clone(); + let job_id_for_frames = options.job_id.clone(); + let mut observed_frame_count = 0u64; + let events = match transport + .collect_model_events_with_frame_observer(request.clone(), move |observation| { + observed_frame_count += 1; + if should_record_frame_observation(observed_frame_count, &observation) { + let mut summary = format!( + "Observed Responses SSE frame {} kind={} recognized={}.", + observation.frame_sequence, observation.kind, observation.recognized + ); + if let Some(preview) = observation.delta_preview.as_deref() { + summary.push_str(" deltaPreview="); + summary.push_str(preview); + } + let _ = append_runtime_event( + &store_path_for_frames, + RuntimeSpineEventOptions { + event_id: format!( + "event-openai-stream-frame-{}-{}", + job_id_for_frames, observation.frame_sequence + ), + occurred_at: now(), + event_type: "openai.model_turn.stream_frame".to_string(), + source: OPENAI_RUNTIME_SOURCE.to_string(), + session_id: Some(session_id_for_frames.clone()), + job_id: Some(job_id_for_frames.clone()), + summary, + }, + ); + } + }) + .await + { Ok(events) => events, Err(err) => { let failure = EpiphanyOpenAiStreamEvent { @@ -201,6 +236,18 @@ pub async fn run_openai_model_turn( record_openai_events(&options.store_path, &options, &request, &events) } +fn should_record_frame_observation( + observed_frame_count: u64, + observation: &EpiphanyResponsesFrameObservation, +) -> bool { + observed_frame_count <= 20 + || observed_frame_count % 100 == 0 + || matches!( + observation.kind.as_str(), + "response.completed" | "response.failed" | "response.incomplete" + ) +} + fn openai_request_input_metrics(request: &EpiphanyOpenAiModelRequest) -> (usize, usize) { let mut chars = 0usize; for item in &request.input { @@ -305,6 +352,7 @@ pub fn record_openai_events( events: &[EpiphanyOpenAiStreamEvent], ) -> Result { let store_path = store_path.as_ref(); + let events = compact_openai_events_for_storage(events); let mut receipt = None; let mut failure = None; { @@ -312,7 +360,7 @@ pub fn record_openai_events( cache.pull_all_backing_stores()?; let model_request = model_request_from_openai_request(DEFAULT_MODEL_PROVIDER, request); cache.put(model_request_key(&model_request.request_id), &model_request)?; - for event in events { + for event in &events { let model_event = model_event_from_openai_event(DEFAULT_MODEL_PROVIDER, event); if let Some(intent) = tool_invocation_intent_from_model_event(&model_event) { cache.put(tool_invocation_intent_key(&intent.intent_id), &intent)?; @@ -339,7 +387,7 @@ pub fn record_openai_events( } } - for event in events { + for event in &events { append_runtime_event( store_path, RuntimeSpineEventOptions { @@ -405,13 +453,85 @@ pub fn record_openai_events( summary, result_id, receipt_id: receipt.map(|item| openai_receipt_key(&item.request_id)), - tool_intent_ids: tool_invocation_intents_from_openai_events(DEFAULT_MODEL_PROVIDER, events) - .into_iter() - .map(|intent| intent.intent_id) - .collect(), + tool_intent_ids: tool_invocation_intents_from_openai_events( + DEFAULT_MODEL_PROVIDER, + &events, + ) + .into_iter() + .map(|intent| intent.intent_id) + .collect(), }) } +fn compact_openai_events_for_storage( + events: &[EpiphanyOpenAiStreamEvent], +) -> Vec { + let mut compacted = Vec::new(); + let mut text_buffer = String::new(); + let mut reasoning_buffer = String::new(); + for event in events { + match &event.payload { + EpiphanyOpenAiStreamPayload::TextDelta { text } => { + flush_reasoning_buffer(&mut compacted, event, &mut reasoning_buffer); + text_buffer.push_str(text); + } + EpiphanyOpenAiStreamPayload::ReasoningDelta { text } => { + flush_text_buffer(&mut compacted, event, &mut text_buffer); + reasoning_buffer.push_str(text); + } + _ => { + flush_text_buffer(&mut compacted, event, &mut text_buffer); + flush_reasoning_buffer(&mut compacted, event, &mut reasoning_buffer); + push_compacted_event(&mut compacted, event, event.payload.clone()); + } + } + } + if let Some(last) = events.last() { + flush_text_buffer(&mut compacted, last, &mut text_buffer); + flush_reasoning_buffer(&mut compacted, last, &mut reasoning_buffer); + } + compacted +} + +fn flush_text_buffer( + compacted: &mut Vec, + source: &EpiphanyOpenAiStreamEvent, + buffer: &mut String, +) { + if !buffer.is_empty() { + let text = std::mem::take(buffer); + push_compacted_event(compacted, source, EpiphanyOpenAiStreamPayload::TextDelta { text }); + } +} + +fn flush_reasoning_buffer( + compacted: &mut Vec, + source: &EpiphanyOpenAiStreamEvent, + buffer: &mut String, +) { + if !buffer.is_empty() { + let text = std::mem::take(buffer); + push_compacted_event( + compacted, + source, + EpiphanyOpenAiStreamPayload::ReasoningDelta { text }, + ); + } +} + +fn push_compacted_event( + compacted: &mut Vec, + source: &EpiphanyOpenAiStreamEvent, + payload: EpiphanyOpenAiStreamPayload, +) { + compacted.push(EpiphanyOpenAiStreamEvent { + schema_id: source.schema_id.clone(), + request_id: source.request_id.clone(), + sequence: compacted.len() as u64, + payload, + }); +} + pub fn load_worker_launch_request( store_path: impl AsRef, job_id: &str, @@ -1326,6 +1446,77 @@ mod tests { use epiphany_openai_adapter::EpiphanyOpenAiModelReceipt; use tempfile::tempdir; + fn test_openai_event( + request_id: &str, + sequence: u64, + payload: EpiphanyOpenAiStreamPayload, + ) -> EpiphanyOpenAiStreamEvent { + EpiphanyOpenAiStreamEvent { + schema_id: epiphany_openai_adapter::OPENAI_ADAPTER_EVENT_SCHEMA_ID.to_string(), + request_id: request_id.to_string(), + sequence, + payload, + } + } + + #[test] + fn compacts_openai_text_and_reasoning_deltas_before_storage() { + let events = vec![ + test_openai_event( + "req-1", + 0, + EpiphanyOpenAiStreamPayload::ReasoningDelta { + text: "think".to_string(), + }, + ), + test_openai_event( + "req-1", + 1, + EpiphanyOpenAiStreamPayload::ReasoningDelta { + text: " small".to_string(), + }, + ), + test_openai_event( + "req-1", + 2, + EpiphanyOpenAiStreamPayload::TextDelta { + text: "{\"role".to_string(), + }, + ), + test_openai_event( + "req-1", + 3, + EpiphanyOpenAiStreamPayload::TextDelta { + text: "Id\":\"modeling\"}".to_string(), + }, + ), + test_openai_event( + "req-1", + 4, + EpiphanyOpenAiStreamPayload::Completed { + receipt: EpiphanyOpenAiModelReceipt::new("req-1", "gpt-5.4"), + }, + ), + ]; + + let compacted = compact_openai_events_for_storage(&events); + + assert_eq!(compacted.len(), 3); + assert_eq!(compacted[0].sequence, 0); + assert!(matches!( + &compacted[0].payload, + EpiphanyOpenAiStreamPayload::ReasoningDelta { text } if text == "think small" + )); + assert!(matches!( + &compacted[1].payload, + EpiphanyOpenAiStreamPayload::TextDelta { text } if text == "{\"roleId\":\"modeling\"}" + )); + assert!(matches!( + compacted[2].payload, + EpiphanyOpenAiStreamPayload::Completed { .. } + )); + } + #[test] fn records_typed_openai_documents_in_runtime_store() -> Result<()> { let temp = tempdir()?; diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 210b9ec9..3597d12b 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -34,7 +34,7 @@ remembers doctrine; the branch remembers the blade. - Do not copy exact branch or HEAD from this note. Run `git status --short --branch` and `git log --oneline -5`. - The repo now has a one-command local operator path: `.\tools\epiphany_local_run.ps1`. Default mode is `smoke`; it builds the retained Codex app-server compatibility edge, builds native Epiphany operator binaries, runs the coordinator smoke, and writes launcher artifacts under `.epiphany-run/` plus coordinator artifacts under `.epiphany-dogfood/`. `-Mode status` is now Epiphany-native: it builds only Epiphany operator binaries, reads `state/thread-state.msgpack` when present, derives status through `epiphany-core`, and does not start or require `codex-app-server`. Use `-Mode plan` or `-Mode run -MaxSteps 4` for the raw bridge-equipped coordinator entrypoints; use `-Mode mvp -FaceInput "" -MaxSteps 4` for the local product cycle that projects Face's character turn, writes a Face/Aquarium bubble, bootstraps a minimal local checkpoint for fresh threads, runs the coordinator with auto-tools, then runs heartbeat sleep/dream maintenance. Live plan/run/mvp modes use the workspace `state/runtime-spine.msgpack`, because CodexThread launch writes worker requests there; the run bundle remains artifact evidence, not runtime lifecycle authority. Add `-ThreadId ` and `-Workspace ` to inspect or continue an existing thread. `status`, `plan`, and `smoke` do not spend model calls; `run` and `mvp` build and use `epiphany-openai-runtime`. Coordinator workers now launch detached and are polled through runtime-spine instead of blocking the coordinator. MVP runs are non-ephemeral by default so `-ThreadId ` can resume and poll later. `-MaxRuntimeSeconds ` is passed into worker runtime; timeout writes a failed worker receipt and exits. Failed modeling now blocks downstream verification until the failed result is reviewed. -- Current Mom-ready blocker: the normal 180-second MVP pass reaches Face -> coordinator -> detached runtime -> heartbeat sleep, but the modeling worker produces no typed result, no tool intents, and empty stdout/stderr before the runtime timeout writes `Worker runtime timed out after 180 seconds.` Native status correctly projects that failure through role result, jobs, roles, and coordinator source signals. Timeout ownership now seals the inner `openai-worker-*` job as well as the outer role worker; use `epiphany-runtime-spine list-jobs` to verify lifecycle invariants. Direct tiny model-turn transport succeeds through the ChatGPT subscription path, and worker events now show `request_prepared` and `transport_ready` before silence. The current suspect is worker prompt/launch shape: about 9.2k instruction chars plus 6.7k input chars still reaches transport and then emits no SSE before timeout even with low reasoning. Next cut should reduce or reshape the worker request, or add deeper stream payload diagnostics if a smaller worker request still stalls. +- Current Mom-ready status: the normal MVP pass reaches Face -> coordinator -> detached runtime -> heartbeat sleep, and the modeling worker can now complete through the ChatGPT subscription transport. The failure was a stack of violated observability/runtime invariants: worker prompts prepended the whole shared persistent-memory rite, Responses frame liveness was invisible until completion, and the runtime tried to persist hundreds of text deltas before completing the worker job. Role/reorient workers now get a compact Epiphany worker boundary instead of full persistent memory; runtime-spine records sampled raw Responses frame kinds with small previews; contiguous reasoning/text deltas compact before durable storage; and a 60-second live proof completed both `openai-worker-2fe38dcd-d99e-4c73-9a58-afcc81f851c7` and worker job `2fe38dcd-d99e-4c73-9a58-afcc81f851c7` with modeling verdict `regather-needed`. Next cut should run the status/acceptance path against that completed modeling result and verify the coordinator advances into verification rather than leaving late detached completion invisible. - Local operator runs now write typed CultMesh intent/receipt documents to `.epiphany-run/cultmesh/operator-runs.ccmp` through `epiphany-operator-run`. The wrapper writes intent before acting and a completion receipt after success; this is the local consent/trace rail, not a hidden scheduler queue. - Status-mode local runs now also write a native CultMesh operator snapshot to `.epiphany-run/cultmesh/operator-snapshots.ccmp` through `epiphany-operator-snapshot`. That snapshot distills the operator-safe MVP status artifact into typed fields such as thread id, state status, coordinator action, CRRC action, reorient action, available actions, and artifact refs; it does not preserve the raw JSON status blob as internal state. - `epiphany-mvp-status --source native` is the current provider-neutral status boundary. It projects scene, pressure, jobs, roles, planning, reorient, CRRC, coordinator status, and runtime-linked worker lifecycle from native core surfaces. It reads runtime-spine job/result receipts for linked jobs so failed workers show as failed in role results, jobs, roles, and coordinator source signals, while successful semantic findings still require typed role/reorient worker result documents. Keep future pluggable model-provider work behind launch/result/runtime documents; do not let provider choice leak into status, prompt authority, scheduler policy, or durable `EpiphanyThreadState`. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 4016095a122318f8afc72e1a0be2f58d108ecdd3..7c310731d232877d4f45f6bdfdd4a7c371030e64 100644 GIT binary patch delta 747 zcmaKpzmC&D5Qnk56D)&tZn{R*oJo@tM=>6^J=InGanLqz}BYt`O@ow}Y zioU(t#;><=^l2N%->+`H`8*v@&yw*hnWj%C=_2IcemIWqY~ptpwoV+OT0?K$iX5;< z*P++efYMkP%bFp#tx@4uwju@Ebm%l151qEvF3StZ+o}RC(Mh5qCpAyTCpFd9t)X$Y zZahmjmxe@~t!T3!$^*D0ZmeNqnIb$jRJtQW%+O+*8U6XD75H1C4m zCcqs4W*#0ix+&58wYLnS(5(mH0%=VNH`)*|24Dt<{k2s$xx<=ZrH$fI*Z;}i`&a0-FQFr+wBR`^(3j*AL!?Lze*;{94fp^6 delta 41 xcmX^5hx6+<&J7Zb+(u?rCKgf07?`SpHY+n0GBVz2p32ldm5FitR3@ee6#zi`4we7_ diff --git a/state/map.yaml b/state/map.yaml index 3617d2d2..1f3b3607 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -72,7 +72,7 @@ active_subgoals: - Use `notes/epiphany-cultmesh-dreaming-roadmap.md` as the design authority for distributed Epiphany dreaming. Public dreams are separately authored typed documents distributed through CultMesh/CultNet; private state remains local and cannot be shared by flipping a visibility flag. Foreign dreams are external thought weather until a reviewed local adoption receipt digests them into memory, planning, doctrine, or governance. - CultMesh is now the preferred ergonomic Rust abstraction over CultCache and CultNet for Epiphany-owned local database surfaces. The Rust substrate is repo-contained under `vendor/cultcache-rs`, `vendor/cultnet-rs`, and `vendor/cultmesh-rs`; the old `E:\Projects\CultLib\crates\*` path is dead in this checkout and must not be reintroduced as a compile-time dependency. Future local Verse/dream/status work should start at CultMesh unless it is explicitly polishing CultMesh internals. Preserve the three-Verse boundary: `epiphany-internal` for private sub-agent typed state, `gamecult-local` for trusted LAN/Yggdrasil-tunnel GameCult project sharing, and `epiphany-global` for untrusted public dreams plus topic-specific threaded public rooms for Face posts. - The active priority is Codex starvation and CultNet liberation. Use `notes/codex-starvation-and-cultnet-liberation-plan.md` as the directive and `notes/codex-auth-spine-inventory.md` as the source-grounded keeper list for the retained OpenAI subscription auth/model transport bridge. Classify all Epiphany-relevant JSON contamination, define the native CultNet runtime boundary, keep worker launch/result/selfPatch cargo as typed documents, evacuate Epiphany operator/state/process authority from Codex JSON-RPC, wrap MCP's unavoidable JSON protocol edge in typed Epiphany intent/result/receipt documents, and keep cutting until Epiphany can run its own state and processes without `codex_message_processor.rs` except for bridge compatibility. - - The next Codex-starvation slice is worker prompt/request reshaping after provider-transport diagnosis. The quarantined Codex MCP spine may execute typed tool intents and emit typed receipts; the model runtime can build and run provider-neutral follow-up turns from those receipts; coordinator run mode passes the quarantined tool adapter into worker runtime with auto-tools enabled by default; worker runtime now enforces max-runtime with one timeout close primitive for both tokio timeout and watchdog paths; inner `openai-worker-*` jobs are opened before async auth/stream waits and sealed with their outer worker on timeout; native status/roles/jobs/coordinator surfaces project failed runtime-linked jobs as terminal without Codex prompt or scheduler authority; direct tiny model-turn transport succeeds; worker requests record request metrics and transport readiness; and the normal worker failure is now isolated to a silent post-transport-ready worker request rather than auth, coordinator scheduling, or basic provider availability. + - The next Codex-starvation slice is post-worker result acceptance/status hardening after the Mom-ready modeling worker proof. The quarantined Codex MCP spine may execute typed tool intents and emit typed receipts; the model runtime can build and run provider-neutral follow-up turns from those receipts; coordinator run mode passes the quarantined tool adapter into worker runtime with auto-tools enabled by default; worker runtime now enforces max-runtime with one timeout close primitive for both tokio timeout and watchdog paths; inner `openai-worker-*` jobs are opened before async auth/stream waits and sealed with their outer worker on timeout; native status/roles/jobs/coordinator surfaces project failed runtime-linked jobs as terminal without Codex prompt or scheduler authority; direct tiny model-turn transport succeeds; worker requests record request metrics, transport readiness, and sampled raw Responses frame kinds; worker prompts no longer prepend the full shared persistent-memory rite; stream deltas compact before durable storage; and a 60-second live MVP proof completed both the OpenAI runtime job and modeling worker job instead of timing out. - The May 2026 teardown/rebuild migration in `notes/epiphany-ideal-architecture-rebuild-plan.md` is closed unless a concrete regression reopens it. Preserve the new foundation: Epiphany view/coordinator policy lives in `epiphany-core`, runtime-spine owns job/result lifecycle identity through `runtime_links`, duplicate acceptance is receipt-keyed, public findings stay typed/sealed, view expansion requires a core contract, and app-server remains routing/adaptation rather than a second policy throne. - Treat Phase 5 update/distill/propose/promote/read/notification as complete enough unless a concrete regression appears. - Preserve read-only boundaries for retrieve, distill, propose, freshness, context, graphQuery, roleResult, reorientResult, and the canonical view lenses for scene, jobs, roles, planning, pressure, reorient, crrc, and coordinator. @@ -569,4 +569,4 @@ current_status: materializes complete MCP-shaped model tool calls as typed tool invocation intents, and the Codex MCP spine can consume those intents from the mixed runtime-spine store and emit typed success/failure receipts. - next_action: Reduce or reshape the worker prompt/launch request so `epiphany-model-runtime run-worker --provider openai-codex` emits a bounded typed result instead of reaching `transport_ready` and then staying silent until timeout. Use `epiphany-runtime-spine list-jobs`, `list-events`, and `list-model-requests` to verify lifecycle, stream, and request-size invariants. + next_action: Run the Mom-ready MVP status/acceptance path against the completed modeling worker result and verify the coordinator advances through verification without treating late worker completion as invisible. Use `epiphany-runtime-spine list-jobs`, `list-events`, and `list-model-requests` to keep lifecycle, stream, and request-size invariants visible.