diff --git a/epiphany-core/src/bin/epiphany-runtime-spine.rs b/epiphany-core/src/bin/epiphany-runtime-spine.rs index 00cd112d..20b498e2 100644 --- a/epiphany-core/src/bin/epiphany-runtime-spine.rs +++ b/epiphany-core/src/bin/epiphany-runtime-spine.rs @@ -2,6 +2,7 @@ use anyhow::Context; use anyhow::Result; use anyhow::anyhow; use chrono::SecondsFormat; +use epiphany_core::EpiphanyRuntimeEvent; use epiphany_core::EpiphanyRuntimeJob; use epiphany_core::EpiphanyRuntimeJobResult; use epiphany_core::RuntimeSpineEventOptions; @@ -18,6 +19,8 @@ use epiphany_core::runtime_spine_cache; use epiphany_core::runtime_spine_status; use epiphany_core::write_runtime_hello_frame; use epiphany_core::write_runtime_schema_catalog_json; +use epiphany_model_adapter::EpiphanyModelInputItem; +use epiphany_model_adapter::EpiphanyModelRequest; use std::env; use std::path::PathBuf; use uuid::Uuid; @@ -98,6 +101,51 @@ fn main() -> Result<()> { ); } } + Command::ListModelRequests => { + let mut cache = runtime_spine_cache(&args.store)?; + cache.pull_all_backing_stores()?; + let mut requests = cache.get_all::()?; + requests.sort_by(|left, right| left.request_id.cmp(&right.request_id)); + println!("model requests"); + for request in requests { + let input_chars = request + .input + .iter() + .map(model_input_item_chars) + .sum::(); + println!( + "{}\t{}\t{}\tinstructions={}\tinputItems={}\tinputChars={}\tcontract={}", + request.request_id, + request.provider, + request.model, + request.instructions.chars().count(), + request.input.len(), + input_chars, + request.output_contract_id.as_deref().unwrap_or("none") + ); + } + } + Command::ListEvents => { + let mut cache = runtime_spine_cache(&args.store)?; + cache.pull_all_backing_stores()?; + let mut events = cache.get_all::()?; + events.sort_by(|left, right| { + left.occurred_at + .cmp(&right.occurred_at) + .then_with(|| left.event_id.cmp(&right.event_id)) + }); + println!("runtime events"); + for event in events { + println!( + "{}\t{}\t{}\t{}\t{}", + event.occurred_at, + event.event_type, + event.job_id.as_deref().unwrap_or("no-job"), + event.source, + event.summary.replace(['\r', '\n', '\t'], " ") + ); + } + } Command::OpenSession { session_id, objective, @@ -223,6 +271,8 @@ enum Command { }, Status, ListJobs, + ListModelRequests, + ListEvents, OpenSession { session_id: String, objective: String, @@ -304,6 +354,8 @@ fn parse_command(mut args: Vec) -> Result { } "status" => Ok(Command::Status), "list-jobs" => Ok(Command::ListJobs), + "list-model-requests" => Ok(Command::ListModelRequests), + "list-events" => Ok(Command::ListEvents), "open-session" => { let mut session_id = format!("session-{}", Uuid::new_v4()); let mut objective = String::new(); @@ -549,5 +601,13 @@ fn now() -> String { } fn usage() -> &'static str { - "usage: epiphany-runtime-spine [--store path] " + "usage: epiphany-runtime-spine [--store path] " +} + +fn model_input_item_chars(item: &EpiphanyModelInputItem) -> usize { + match item { + EpiphanyModelInputItem::UserText { text } + | EpiphanyModelInputItem::AssistantText { text } => text.chars().count(), + EpiphanyModelInputItem::ToolResult { output, .. } => output.chars().count(), + } } diff --git a/epiphany-openai-runtime/src/lib.rs b/epiphany-openai-runtime/src/lib.rs index 4022b22f..ab100e45 100644 --- a/epiphany-openai-runtime/src/lib.rs +++ b/epiphany-openai-runtime/src/lib.rs @@ -143,10 +143,45 @@ pub async fn run_openai_model_turn( summary: format!("Started typed OpenAI request {}.", request.request_id), }, )?; + let (input_items, input_chars) = openai_request_input_metrics(&request); + append_runtime_event( + &options.store_path, + RuntimeSpineEventOptions { + event_id: format!("event-openai-request-prepared-{}", options.job_id), + occurred_at: now(), + event_type: "openai.model_turn.request_prepared".to_string(), + source: OPENAI_RUNTIME_SOURCE.to_string(), + session_id: Some(options.session_id.clone()), + job_id: Some(options.job_id.clone()), + summary: format!( + "Prepared OpenAI request {} for model {}: instructions={} chars, inputItems={}, inputChars={}.", + request.request_id, + request.model, + request.instructions.chars().count(), + input_items, + input_chars + ), + }, + )?; let status = status_from_auth_manager(&auth_manager, options.default_model.clone(), true).await; store_openai_status(&options.store_path, &status)?; store_model_status(&options.store_path, &status, DEFAULT_MODEL_PROVIDER)?; + append_runtime_event( + &options.store_path, + RuntimeSpineEventOptions { + event_id: format!("event-openai-transport-ready-{}", options.job_id), + occurred_at: now(), + event_type: "openai.model_turn.transport_ready".to_string(), + source: OPENAI_RUNTIME_SOURCE.to_string(), + session_id: Some(options.session_id.clone()), + job_id: Some(options.job_id.clone()), + summary: format!( + "Codex/OpenAI transport ready for request {} with auth mode {:?}; opening Responses stream.", + request.request_id, status.auth_mode + ), + }, + )?; let transport = EpiphanyCodexOpenAiTransport::openai(auth_manager); let events = match transport.collect_model_events(request.clone()).await { @@ -166,6 +201,18 @@ pub async fn run_openai_model_turn( record_openai_events(&options.store_path, &options, &request, &events) } +fn openai_request_input_metrics(request: &EpiphanyOpenAiModelRequest) -> (usize, usize) { + let mut chars = 0usize; + for item in &request.input { + chars += match item { + EpiphanyOpenAiInputItem::UserText { text } + | EpiphanyOpenAiInputItem::AssistantText { text } => text.chars().count(), + EpiphanyOpenAiInputItem::ToolResult { output, .. } => output.chars().count(), + }; + } + (request.input.len(), chars) +} + pub async fn run_model_turn( provider: &str, options: EpiphanyOpenAiRuntimeOptions, @@ -401,6 +448,8 @@ pub fn build_worker_model_request( "Execute this Epiphany worker launch document.\n\n```json\n{launch_document_text}\n```" ), }); + request.reasoning_effort = Some("low".to_string()); + request.reasoning_summary = Some("concise".to_string()); request.output_contract_id = Some(launch_request.output_contract_id.clone()); Ok(request) } @@ -1405,6 +1454,8 @@ mod tests { model_request.output_contract_id.as_deref(), Some(epiphany_core::ROLE_WORKER_OUTPUT_CONTRACT_ID) ); + assert_eq!(model_request.reasoning_effort.as_deref(), Some("low")); + assert_eq!(model_request.reasoning_summary.as_deref(), Some("concise")); let openai_summary = EpiphanyOpenAiRuntimeRunSummary { store: store.display().to_string(), session_id: "openai-worker-session-modeling-checkpoint-worker".to_string(), diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 6c132339..210b9ec9 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. Next cut should diagnose provider stream/request observability or prompt/request size in `epiphany-model-runtime run-worker --provider openai-codex`, not coordinator scheduling. +- 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. - 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 598c9faa..4016095a 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index b468a5ca..3617d2d2 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 provider-transport diagnosis after the Mom-ready normal pass. 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; and the normal 180-second MVP pass now fails cleanly at the provider/runtime stream seam instead of blocking coordinator or hiding as pending state. + - 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 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: Diagnose why `epiphany-model-runtime run-worker --provider openai-codex` produces no typed worker result, no tool intents, and empty worker stdout/stderr before the 180-second timeout fires in the normal Mom-ready MVP pass. The timeout ownership leak is repaired; use `epiphany-runtime-spine list-jobs` to check job lifecycle invariants while investigating provider stream observability or prompt/request sizing. + 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.