diff --git a/epiphany-core/src/runtime_spine.rs b/epiphany-core/src/runtime_spine.rs index 4c62ef3a..42c32fb7 100644 --- a/epiphany-core/src/runtime_spine.rs +++ b/epiphany-core/src/runtime_spine.rs @@ -549,6 +549,8 @@ pub struct EpiphanyToolInvocationStatus { pub adapter: String, pub server: String, pub tool_name: String, + pub call_id: Option, + pub model_request_id: Option, pub caller: String, pub reason: String, pub created_at: String, @@ -1351,6 +1353,8 @@ pub fn runtime_tool_invocation_statuses( adapter: intent.adapter, server: intent.server, tool_name: intent.tool_name, + call_id: intent.call_id, + model_request_id: intent.model_request_id, caller: intent.caller, reason: intent.reason, created_at: intent.created_at, diff --git a/epiphany-model-adapter/src/lib.rs b/epiphany-model-adapter/src/lib.rs index 881bdfb7..49240082 100644 --- a/epiphany-model-adapter/src/lib.rs +++ b/epiphany-model-adapter/src/lib.rs @@ -52,6 +52,8 @@ pub struct EpiphanyModelRequest { pub service_tier: Option, #[cultcache(key = 10, default)] pub output_contract_id: Option, + #[cultcache(key = 11, default)] + pub previous_response_id: Option, } impl EpiphanyModelRequest { @@ -74,6 +76,7 @@ impl EpiphanyModelRequest { reasoning_summary: None, service_tier: None, output_contract_id: None, + previous_response_id: None, } } } diff --git a/epiphany-openai-adapter/src/lib.rs b/epiphany-openai-adapter/src/lib.rs index 6009aafe..9ad0b627 100644 --- a/epiphany-openai-adapter/src/lib.rs +++ b/epiphany-openai-adapter/src/lib.rs @@ -65,6 +65,8 @@ pub struct EpiphanyOpenAiModelRequest { pub service_tier: Option, #[cultcache(key = 9, default)] pub output_contract_id: Option, + #[cultcache(key = 10, default)] + pub previous_response_id: Option, } impl EpiphanyOpenAiModelRequest { @@ -85,6 +87,7 @@ impl EpiphanyOpenAiModelRequest { reasoning_summary: None, service_tier: None, output_contract_id: None, + previous_response_id: None, } } } diff --git a/epiphany-openai-codex-spine/src/lib.rs b/epiphany-openai-codex-spine/src/lib.rs index cfac4f55..fafdeaf6 100644 --- a/epiphany-openai-codex-spine/src/lib.rs +++ b/epiphany-openai-codex-spine/src/lib.rs @@ -204,6 +204,7 @@ pub fn responses_body_from_epiphany( stream: true, include: Vec::new(), service_tier: parse_service_tier(request.service_tier.as_deref())?, + previous_response_id: request.previous_response_id, prompt_cache_key: None, text: None, client_metadata: None, @@ -227,6 +228,8 @@ struct EpiphanyResponsesBody { #[serde(skip_serializing_if = "Option::is_none")] service_tier: Option, #[serde(skip_serializing_if = "Option::is_none")] + previous_response_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] prompt_cache_key: Option, #[serde(skip_serializing_if = "Option::is_none")] text: Option, @@ -562,11 +565,13 @@ mod tests { request.reasoning_effort = Some("low".to_string()); request.reasoning_summary = Some("concise".to_string()); request.service_tier = Some("flex".to_string()); + request.previous_response_id = Some("resp-1".to_string()); let responses = responses_body_from_epiphany(request).expect("request should map"); assert_eq!(responses["model"], "gpt-5.4"); assert_eq!(responses["instructions"], "Answer plainly."); + assert_eq!(responses["previous_response_id"], "resp-1"); assert_eq!(responses["input"][0]["content"][0]["type"], "input_text"); assert_eq!(responses["stream"], true); assert_eq!(responses["service_tier"], "flex"); diff --git a/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs b/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs index cc14e4bd..eb3e6d14 100644 --- a/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs +++ b/epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs @@ -15,6 +15,7 @@ use epiphany_openai_runtime::EpiphanyOpenAiRuntimeOptions; use epiphany_openai_runtime::EpiphanyWorkerRuntimeOptions; use epiphany_openai_runtime::OPENAI_RUNTIME_ROLE; use epiphany_openai_runtime::assistant_text_from_model_events; +use epiphany_openai_runtime::build_tool_followup_model_request; use epiphany_openai_runtime::default_codex_home; use epiphany_openai_runtime::default_options; use epiphany_openai_runtime::ensure_openai_runtime_ready; @@ -77,6 +78,26 @@ async fn main() -> Result<()> { .await?; print_json(&summary)?; } + "tool-followup" => { + let options = parse_tool_followup_options(args.collect())?; + let request = build_tool_followup_model_request( + &options.store_path, + &options.request_id, + &options.followup_request_id, + )?; + if let Some(parent) = options.output.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("failed to create {}", parent.display()))?; + } + fs::write(&options.output, serde_json::to_string_pretty(&request)?) + .with_context(|| format!("failed to write {}", options.output.display()))?; + print_json(&json!({ + "requestId": request.request_id, + "previousResponseId": request.previous_response_id, + "inputItems": request.input.len(), + "output": options.output, + }))?; + } "smoke" => { let options = parse_smoke_options(args.collect())?; require_supported_provider(&options.provider)?; @@ -218,6 +239,13 @@ struct RunWorkerCliOptions { model: String, } +struct ToolFollowupCliOptions { + store_path: PathBuf, + request_id: String, + followup_request_id: String, + output: PathBuf, +} + fn parse_model_turn_options(args: Vec) -> Result { let mut provider = DEFAULT_PROVIDER.to_string(); let mut store_path = PathBuf::from(DEFAULT_STORE); @@ -287,6 +315,32 @@ fn parse_run_worker_options(args: Vec) -> Result { }) } +fn parse_tool_followup_options(args: Vec) -> Result { + let mut store_path = PathBuf::from(DEFAULT_STORE); + let mut request_id = None; + let mut followup_request_id = None; + let mut output = None; + let mut iter = args.into_iter(); + while let Some(arg) = iter.next() { + match arg.as_str() { + "--store" => store_path = PathBuf::from(next_value(&mut iter, "--store")?), + "--request-id" => request_id = Some(next_value(&mut iter, "--request-id")?), + "--followup-request-id" => { + followup_request_id = Some(next_value(&mut iter, "--followup-request-id")?) + } + "--output" => output = Some(PathBuf::from(next_value(&mut iter, "--output")?)), + other => return Err(anyhow!("unknown tool-followup argument: {other}")), + } + } + Ok(ToolFollowupCliOptions { + store_path, + request_id: request_id.context("tool-followup requires --request-id")?, + followup_request_id: followup_request_id + .unwrap_or_else(|| format!("tool-followup-{}", Uuid::new_v4())), + output: output.context("tool-followup requires --output")?, + }) +} + fn parse_smoke_options(args: Vec) -> Result { let mut provider = DEFAULT_PROVIDER.to_string(); let mut store_path = PathBuf::from(format!( @@ -334,5 +388,5 @@ fn now() -> String { } fn usage() -> &'static str { - "usage: epiphany-model-runtime [--provider openai-codex] [--store path] [--codex-home path] [--request path] [--session-id id] [--job-id id] [--objective text] [--default-model model] [--output-last-message path]" + "usage: epiphany-model-runtime [--provider openai-codex] [--store path] [--codex-home path] [--request path] [--request-id id] [--followup-request-id id] [--output path] [--session-id id] [--job-id id] [--objective text] [--default-model model] [--output-last-message path]" } diff --git a/epiphany-openai-runtime/src/lib.rs b/epiphany-openai-runtime/src/lib.rs index b888bd73..c853510f 100644 --- a/epiphany-openai-runtime/src/lib.rs +++ b/epiphany-openai-runtime/src/lib.rs @@ -39,7 +39,9 @@ pub use epiphany_openai_codex_spine::default_codex_home; use epiphany_openai_codex_spine::status_from_auth_manager; use epiphany_tool_adapter::CODEX_MCP_TOOL_ADAPTER_ID; use epiphany_tool_adapter::EpiphanyToolInvocationIntent; +use epiphany_tool_adapter::EpiphanyToolInvocationReceipt; use epiphany_tool_adapter::tool_invocation_intent_key; +use epiphany_tool_adapter::tool_invocation_receipt_key; use serde::de::DeserializeOwned; pub const OPENAI_RUNTIME_ROLE: &str = "openai-model-adapter"; @@ -572,6 +574,87 @@ pub fn assistant_text_from_model_events( Ok(text) } +pub fn build_tool_followup_model_request( + store_path: impl AsRef, + original_request_id: &str, + followup_request_id: &str, +) -> Result { + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + let original = cache + .get::(&model_request_key(original_request_id))? + .ok_or_else(|| anyhow!("model request {original_request_id:?} does not exist"))?; + let receipt = cache + .get::(&model_receipt_key(original_request_id))? + .ok_or_else(|| anyhow!("model receipt {original_request_id:?} does not exist"))?; + let previous_response_id = receipt.provider_response_id.clone().ok_or_else(|| { + anyhow!("model receipt {original_request_id:?} has no provider_response_id") + })?; + let original_prefix = format!("model-{}-", sanitize_request_id(original_request_id)); + let mut followup_items = Vec::new(); + for intent in cache.get_all::()? { + if intent.model_request_id.as_deref() != Some(original_request_id) + && !intent.intent_id.starts_with(&original_prefix) + { + continue; + } + let Some(call_id) = intent.call_id.clone() else { + continue; + }; + let Some(receipt) = cache.get::( + &tool_invocation_receipt_key(&intent.intent_id), + )? + else { + continue; + }; + followup_items.push((intent, call_id, receipt)); + } + followup_items.sort_by(|left, right| { + left.0 + .created_at + .cmp(&right.0.created_at) + .then_with(|| left.0.intent_id.cmp(&right.0.intent_id)) + }); + if followup_items.is_empty() { + return Err(anyhow!( + "model request {original_request_id:?} has no completed tool receipts with call ids" + )); + } + + let mut followup = original; + followup.request_id = followup_request_id.to_string(); + followup.previous_response_id = Some(previous_response_id); + followup.input = followup_items + .into_iter() + .map( + |(intent, call_id, receipt)| EpiphanyModelInputItem::ToolResult { + call_id, + output: tool_receipt_output_for_model(&intent, &receipt), + }, + ) + .collect(); + Ok(followup) +} + +fn tool_receipt_output_for_model( + intent: &EpiphanyToolInvocationIntent, + receipt: &EpiphanyToolInvocationReceipt, +) -> String { + if let Some(result) = receipt.result_json.as_ref() { + return result.clone(); + } + serde_json::json!({ + "status": receipt.status, + "adapter": receipt.adapter, + "server": receipt.server, + "toolName": receipt.tool_name, + "intentId": intent.intent_id, + "receiptId": receipt.receipt_id, + "error": receipt.error, + }) + .to_string() +} + pub fn default_options( store_path: PathBuf, codex_home: PathBuf, @@ -670,6 +753,7 @@ pub fn model_request_from_openai_request( reasoning_summary: request.reasoning_summary.clone(), service_tier: request.service_tier.clone(), output_contract_id: request.output_contract_id.clone(), + previous_response_id: request.previous_response_id.clone(), } } @@ -691,6 +775,7 @@ pub fn openai_request_from_model_request( reasoning_summary: request.reasoning_summary.clone(), service_tier: request.service_tier.clone(), output_contract_id: request.output_contract_id.clone(), + previous_response_id: request.previous_response_id.clone(), } } @@ -810,24 +895,27 @@ pub fn tool_invocation_intent_from_model_event( if !arguments_are_invocation_ready(arguments) { return None; } - Some(EpiphanyToolInvocationIntent::new( - format!( - "model-{}-{}-{}", - sanitize_request_id(&event.request_id), - event.sequence, - sanitize_request_id(call_id) - ), - CODEX_MCP_TOOL_ADAPTER_ID, - server, - tool_name, - arguments.clone(), - format!("model-runtime:{}", event.provider), - format!( - "Model request {} emitted MCP tool call {}.", - event.request_id, call_id - ), - now(), - )) + Some( + EpiphanyToolInvocationIntent::new( + format!( + "model-{}-{}-{}", + sanitize_request_id(&event.request_id), + event.sequence, + sanitize_request_id(call_id) + ), + CODEX_MCP_TOOL_ADAPTER_ID, + server, + tool_name, + arguments.clone(), + format!("model-runtime:{}", event.provider), + format!( + "Model request {} emitted MCP tool call {}.", + event.request_id, call_id + ), + now(), + ) + .with_model_call(call_id.clone(), event.request_id.clone()), + ) } fn split_mcp_tool_name(name: &str) -> Option<(String, String)> { @@ -1340,6 +1428,99 @@ mod tests { assert_eq!(intent.tool_name, "list_events"); assert_eq!(intent.arguments_json, r#"{"limit":3}"#); assert_eq!(intent.intent_id, "model-request-1-7-call-1"); + assert_eq!(intent.call_id.as_deref(), Some("call/1")); + assert_eq!(intent.model_request_id.as_deref(), Some("request-1")); + Ok(()) + } + + #[test] + fn builds_tool_followup_model_request_from_receipts() -> Result<()> { + let temp = tempdir()?; + let store = temp.path().join("runtime.msgpack"); + let request = EpiphanyOpenAiModelRequest::new( + "req-tools", + "conversation-1", + "gpt-5.4", + "Answer after tool output.", + ); + let options = default_options(store.clone(), PathBuf::from(".codex"), &request); + ensure_openai_runtime_ready(&options)?; + ensure_runtime_session( + &store, + RuntimeSpineSessionOptions { + session_id: options.session_id.clone(), + objective: options.objective.clone(), + created_at: now(), + coordinator_note: options.coordinator_note.clone(), + }, + )?; + create_runtime_job( + &store, + RuntimeSpineJobOptions { + job_id: options.job_id.clone(), + session_id: options.session_id.clone(), + role: OPENAI_RUNTIME_ROLE.to_string(), + created_at: now(), + summary: "tool test job".to_string(), + artifact_refs: Vec::new(), + }, + )?; + let mut receipt = EpiphanyOpenAiModelReceipt::new("req-tools", "gpt-5.4"); + receipt.response_id = Some("resp-tools".to_string()); + receipt.transport = Some("test".to_string()); + let events = vec![ + EpiphanyOpenAiStreamEvent { + schema_id: epiphany_openai_adapter::OPENAI_ADAPTER_EVENT_SCHEMA_ID.to_string(), + request_id: "req-tools".to_string(), + sequence: 0, + payload: EpiphanyOpenAiStreamPayload::ToolCall { + call_id: "call-original".to_string(), + name: "mcp__smoke_server__smoke_tool".to_string(), + arguments: "{}".to_string(), + }, + }, + EpiphanyOpenAiStreamEvent { + schema_id: epiphany_openai_adapter::OPENAI_ADAPTER_EVENT_SCHEMA_ID.to_string(), + request_id: "req-tools".to_string(), + sequence: 1, + payload: EpiphanyOpenAiStreamPayload::Completed { receipt }, + }, + ]; + let summary = record_openai_events(&store, &options, &request, &events)?; + let intent_id = summary + .tool_intent_ids + .first() + .expect("tool intent id") + .clone(); + let mut cache = runtime_spine_cache(&store)?; + cache.pull_all_backing_stores()?; + let mut tool_receipt = EpiphanyToolInvocationReceipt::new( + "receipt-tool", + intent_id.clone(), + epiphany_tool_adapter::CODEX_MCP_TOOL_ADAPTER_ID, + "smoke_server", + "smoke_tool", + "completed", + now(), + ); + tool_receipt.result_json = Some(r#"{"ok":true}"#.to_string()); + cache.put( + tool_invocation_receipt_key(&tool_receipt.intent_id), + &tool_receipt, + )?; + + let followup = + build_tool_followup_model_request(&store, "req-tools", "req-tools-followup")?; + assert_eq!(followup.request_id, "req-tools-followup"); + assert_eq!(followup.previous_response_id.as_deref(), Some("resp-tools")); + assert_eq!(followup.input.len(), 1); + assert_eq!( + followup.input[0], + EpiphanyModelInputItem::ToolResult { + call_id: "call-original".to_string(), + output: r#"{"ok":true}"#.to_string() + } + ); Ok(()) } diff --git a/epiphany-tool-adapter/src/lib.rs b/epiphany-tool-adapter/src/lib.rs index f1309971..5643c788 100644 --- a/epiphany-tool-adapter/src/lib.rs +++ b/epiphany-tool-adapter/src/lib.rs @@ -78,6 +78,10 @@ pub struct EpiphanyToolInvocationIntent { pub reason: String, #[cultcache(key = 8)] pub created_at: String, + #[cultcache(key = 9, default)] + pub call_id: Option, + #[cultcache(key = 10, default)] + pub model_request_id: Option, } impl EpiphanyToolInvocationIntent { @@ -101,8 +105,20 @@ impl EpiphanyToolInvocationIntent { caller: caller.into(), reason: reason.into(), created_at: created_at.into(), + call_id: None, + model_request_id: None, } } + + pub fn with_model_call( + mut self, + call_id: impl Into, + model_request_id: impl Into, + ) -> Self { + self.call_id = Some(call_id.into()); + self.model_request_id = Some(model_request_id.into()); + self + } } #[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 86bdae2f..9ff221e8 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -40,7 +40,7 @@ remembers doctrine; the branch remembers the blade. - `epiphany-model-runtime` is the current provider-neutral executable boundary. It is built by the `epiphany-openai-runtime` crate for now and accepts `--provider openai-codex`; the old `epiphany-openai-runtime` binary, `--openai-runtime-bin`, and `openai-runtime` executor name survive only as compatibility aliases. Coordinator run mode, local wrapper run mode, and repo birth runner should call `--model-runtime-bin` plus `--model-provider`. The typed request/event/receipt contract is now provider-neutral in `epiphany-model-adapter` as `epiphany.model_request.v0`, `epiphany.model_stream_event.v0`, `epiphany.model_receipt.v0`, and `epiphany.model_adapter_status.v0`; OpenAI/Codex documents are adapter evidence behind that boundary, not scheduler/status/state authority. - Coordinator plan/run passes now emit `epiphany.coordinator_run_receipt.v0` into runtime-spine. `coordinator-summary.json` and `coordinator-steps.jsonl` remain useful operator artifacts, but the durable receipt of what the coordinator decided is typed CultCache/CultNet state. - The first tool/MCP contract surface exists in `epiphany-tool-adapter`: `epiphany.tool_capability.v0`, `epiphany.tool_invocation_intent.v0`, and `epiphany.tool_invocation_receipt.v0`. Runtime-spine advertises these as the Epiphany-facing tool boundary. `epiphany-model-runtime` now materializes complete MCP-shaped model tool calls as typed invocation intents, and `epiphany-tool-codex-mcp-spine` is the quarantined executing adapter: it consumes one typed invocation intent from the mixed runtime-spine CultCache store, calls user-declared Codex MCP through `codex-mcp`, and emits a typed invocation receipt. Raw MCP JSON remains protocol-edge cargo; Codex MCP is transport scaffolding, not Epiphany state, prompt, scheduler, or policy authority. -- Native status now exposes that quarantined bridge spine without granting it authority. `runtime_spine_status` counts tool invocation intents, receipts, and pending calls; `runtime_tool_invocation_statuses` derives read-only intent/receipt status rows; `epiphany-runtime-spine status` reports the counts; and `epiphany-mvp-status --runtime-store ` renders the latest tool calls under `tools`. This is operator visibility only. The next clean backend slice is the typed tool-result feedback path into model continuation, with Epiphany-owned prompt/result policy and no Codex prompt machinery instructing Epiphany agents. +- Native status now exposes that quarantined bridge spine without granting it authority. `runtime_spine_status` counts tool invocation intents, receipts, and pending calls; `runtime_tool_invocation_statuses` derives read-only intent/receipt status rows; `epiphany-runtime-spine status` reports the counts; and `epiphany-mvp-status --runtime-store ` renders the latest tool calls under `tools`. This is operator visibility only. The model-runtime feedback seam is also now typed: MCP-shaped model tool calls preserve the original provider `call_id` plus source model request id, and `epiphany-model-runtime tool-followup` builds a provider-neutral follow-up `EpiphanyModelRequest` from completed typed tool receipts using the previous provider response id. Codex still executes the quarantined MCP edge; Epiphany owns the continuation request shape. - The active foundation directive is now `notes/codex-starvation-and-cultnet-liberation-plan.md`. The previous Codex app-server control-plane rebuild made Epiphany less rotten inside Codex, but that is not the destination. Epiphany must become a native CultCache/CultMesh/CultNet runtime while remaining an honest modified Codex-derived backend for subscription auth/model use. Codex should remain relatively vanilla and keep doing Codex things, including useful app-server and streaming bridge affordances, but it must not own Epiphany state, processes, prompts, scheduler decisions, or policy. The bridge handles interop; Codex and Epiphany do not get to rummage around in each other's organs. - `notes/epiphany-cultmesh-dreaming-roadmap.md` is the new concrete design for distributed Epiphany dreaming. The invariant is hard: private state stays local; public dreams are separately authored typed documents distributed through CultMesh/CultNet; foreign dreams are thought weather until a reviewed local adoption receipt digests them. The first code slice should be dream schemas plus a local CultMesh-backed store, not public fanout. - CultMesh is now the preferred Rust abstraction for that local store work. The compile-time Rust substrate is repo-contained: `vendor/cultcache-rs`, `vendor/cultnet-rs`, and `vendor/cultmesh-rs`. The old `E:\Projects\CultLib\crates\*` dependency body is dead in this checkout and must not be revived by accident. Vendored CultCache reads/writes `cultcache.store.v1`, so `state/ledgers.msgpack` and `epiphany-state status` are aligned again. The first integrations are deliberately small: status, Verse policy, global room policy, organ contract policy, native operator status, native operator snapshots, and local operator run intent/receipt round-trip through CultMesh. Use `cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-cultmesh-smoke`, `cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-cultmesh-status -- smoke --store .\.epiphany-smoke\cultmesh\operator-status.ccmp --runtime-id epiphany-cultmesh-status-smoke`, `cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-operator-snapshot -- smoke --store .\.epiphany-smoke\cultmesh\operator-snapshots.ccmp --runtime-id epiphany-operator-snapshot-smoke --snapshot-id smoke-status`, and `cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-operator-run -- smoke --store .\.epiphany-smoke\cultmesh\operator-runs.ccmp --runtime-id epiphany-operator-run-smoke --run-id smoke-run` for the focused proofs. Future local dream/status/Verse work should start from CultMesh, not raw CultNet, unless the task is polishing the CultMesh internals themselves. Preserve the three Verse tiers: `epiphany-internal` for private sub-agent typed state, `gamecult-local` for trusted LAN/Yggdrasil-tunnel GameCult sharing, and `epiphany-global` for untrusted public dreams plus topic-specific threaded public rooms for Face posts. @@ -183,7 +183,7 @@ remembers doctrine; the branch remembers the blade. - The planning loop is now runtime-backed and operator-visible. Typed captures, backlog items, roadmap streams, Objective Drafts, and GitHub issue source refs live in `EpiphanyThreadState`, validate through revision-gated `thread/epiphany/update`, render into prompts when present, project read-only through the `planning` view lens, render in the GUI Planning panel, can be synthesized by the fixed Imagination/planning lane, and can be explicitly adopted through the artifacted `adoptObjectiveDraft` GUI action. Chat is deliberation, not an objective pipe; ideas and GitHub Issues remain planning state until explicit human adoption. - The bridge/dashboard slice is now landed far enough to test locally. Native `epiphany-rider-bridge` inspects Rider installation, solution, VCS, changed files, and captures file/selection/symbol context into `.epiphany-gui/rider` artifacts; Aquarium has Inspect Rider and renders Rider audit artifacts in Environment; Aquarium also embeds the adjacent EpiphanyGraph viewer over typed `graphs.architecture`, `graphs.dataflow`, and `graphs.links`. - A first Rider plugin scaffold lives under `integrations/rider`: tool window, Refresh status, and Send Context to Epiphany action. It shells through native `epiphany-rider-bridge` and does not own state. `gradle` is not installed on this machine, so the scaffold is not build-verified yet. -- Next real move: continue Codex starvation and native Epiphany ownership by wiring model-tool receipts back into typed model continuation. Quarantine Rider/Unity bridge work for now; those artifacts remain optional later environment evidence, not MVP blockers. Keep EpiphanyAgent focused on backend contracts, typed state, coordinator policy, heartbeat scheduling, Face guardrails, prompt-authority separation, and the ongoing purification from Python/JSON scaffolding into Rust/CultCache/CultMesh/CultNet organs. Resume repo birth/Aquarium/Rider/Unity work only when the user explicitly shifts priority back outward. GitHub Issues import is deferred until the backlog source is fresh enough to deserve Imagination's attention. +- Next real move: continue Codex starvation and native Epiphany ownership by making the coordinator/runtime loop consume the typed `tool-followup` request instead of requiring the operator to run the three commands manually. Quarantine Rider/Unity bridge work for now; those artifacts remain optional later environment evidence, not MVP blockers. Keep EpiphanyAgent focused on backend contracts, typed state, coordinator policy, heartbeat scheduling, Face guardrails, prompt-authority separation, and the ongoing purification from Python/JSON scaffolding into Rust/CultCache/CultMesh/CultNet organs. Resume repo birth/Aquarium/Rider/Unity work only when the user explicitly shifts priority back outward. GitHub Issues import is deferred until the backlog source is fresh enough to deserve Imagination's attention. - The repo is an Epiphany fork of Codex, not a Codex preset. - `vendor/codex` is tracked directly, not a submodule. - `epiphany-core` owns the heavy Epiphany organs where practical. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index fe8e050b..b8ed5242 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index d7719194..5bd393b8 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 typed tool-result feedback into model continuation. The quarantined Codex MCP spine may execute typed tool intents and emit typed receipts, but the runtime/status surface must remain read-only until Epiphany owns the continuation policy. Codex transports tool calls; it must not inject prompt doctrine or decide what Epiphany agents do with tool results. + - The next Codex-starvation slice is automatic coordinator/runtime consumption of typed tool-result follow-up requests. The quarantined Codex MCP spine may execute typed tool intents and emit typed receipts, and the model runtime can now build a provider-neutral follow-up request from those receipts, but the coordinator still needs to own when that follow-up turn runs. Codex transports tool calls; it must not inject prompt doctrine or decide what Epiphany agents do with tool results. - 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. @@ -177,6 +177,7 @@ accepted_design: - Pre-compaction checkpoint intervention is now wired at token-count boundaries: when a loaded Epiphany thread crosses the existing `shouldPrepareCompaction` pressure threshold during an active turn, the harness steers the active agent once for that turn with a bounded CRRC directive to bank scratch/checkpoint/map/evidence before compaction/reorientation. A successful steer marks a turn-scoped pending compact handoff consumed at clean turn completion; this is banking the fire before the dark, not semantic acceptance, broad scheduling, or automatic implementation continuation. - Native `epiphany-mvp-status` is the first dogfood operator view: it starts or reads a thread through app-server and renders scene, planning, pressure, reorient, jobs, role result read-backs, reorient result, CRRC recommendation, heartbeat, Face bubbles, and MVP role lanes as text or machine output without becoming a state store. The old Python status module has been cut; native Rust/CultCache/CultNet surfaces are the product path. - Native status now exposes the quarantined tool/MCP spine as operator-visible read-only state. Runtime-spine counts typed tool invocation intents, receipts, and pending calls; `runtime_tool_invocation_statuses` derives intent/receipt status rows; `epiphany-runtime-spine status` prints the counts; and `epiphany-mvp-status --runtime-store ` renders a `tools` section without executing calls, accepting results, or letting Codex prompt machinery steer Epiphany agents. + - Model-tool feedback now has a typed continuation seam. `EpiphanyToolInvocationIntent` preserves the exact model `call_id` plus source model request id; provider-neutral and OpenAI request documents carry optional `previous_response_id`; the OpenAI Responses mapper emits that id as `previous_response_id`; and `epiphany-model-runtime tool-followup` builds a provider-neutral follow-up request from typed tool receipts. The manual smoke path is model-runtime smoke -> quarantined Codex MCP spine run -> model-runtime `tool-followup`; the next organ is coordinator ownership of that sequence. - The `roles` view lens is the harness-native role ownership surface: it projects implementation, Imagination/planning, modeling/checkpoint, verification/review, and reorientation lanes from authoritative typed state, bound jobs, pressure/reorient/CRRC, and worker result status without launching, scheduling, accepting, compacting, or mutating state. - `thread/epiphany/roleLaunch` and `thread/epiphany/roleResult` are the first explicit role-scoped specialist templates: Imagination/planning, modeling/checkpoint, and verification/review launch through fixed heartbeat-backed bindings with typed runtime-spine receipts and can be read back as reviewable findings from typed runtime-spine job results, while implementation stays owned by the main coding agent and reorientation stays on the CRRC-specific launch/result path. - `thread/epiphany/roleAccept` is the first review-gated role finding acceptance path: it accepts completed heartbeat-backed findings from typed runtime-spine results, accepts Imagination/planning findings only when they return a valid planning-only `statePatch` with draft Objective Draft review gates, accepts completed modeling/checkpoint findings only with a modeling-safe `statePatch`, builds accepted evidence/observation/receipt bundles through `epiphany-core`, and rejects verification, implementation, reorientation, job binding, objective adoption, or broad authority fields. @@ -552,4 +553,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: Continue Codex starvation and CultNet liberation by wiring typed tool invocation receipts back into model continuation through an Epiphany-owned policy path; the pending-tool operator visibility surface is landed, so the next fork is how follow-up model input is owned without Codex prompt machinery steering Epiphany agents. + next_action: Continue Codex starvation and CultNet liberation by making the coordinator/runtime loop consume the typed tool-followup request path automatically, preserving Epiphany-owned continuation policy while Codex remains only auth/model/MCP transport.