From b95b95c11d53c8edd0309aaa7ccf85b1e26878f9 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 17 May 2026 20:52:00 +0100 Subject: [PATCH] Move state update proof to core --- epiphany-core/src/runtime_spine.rs | 54 ++ epiphany-core/src/state_update.rs | 473 +++++++++++++ notes/fresh-workspace-handoff.md | 1 + state/ledgers.msgpack | Bin 118392 -> 118671 bytes state/map.yaml | 11 +- .../codex/codex-rs/core/src/codex_thread.rs | 646 ------------------ 6 files changed, 533 insertions(+), 652 deletions(-) diff --git a/epiphany-core/src/runtime_spine.rs b/epiphany-core/src/runtime_spine.rs index 6aaa513f..13c52553 100644 --- a/epiphany-core/src/runtime_spine.rs +++ b/epiphany-core/src/runtime_spine.rs @@ -1964,6 +1964,60 @@ mod tests { Ok(()) } + #[test] + fn heartbeat_launch_plan_leaves_lifecycle_to_runtime_links() { + let launch_plan = plan_runtime_spine_heartbeat_launch( + &EpiphanyThreadState::default(), + RuntimeSpineHeartbeatLaunchPlanOptions { + binding_id: "modeling-checkpoint-worker".to_string(), + kind: EpiphanyJobKind::Specialist, + scope: "role-scoped modeling/checkpoint maintenance".to_string(), + owner_role: "epiphany-modeler".to_string(), + authority_scope: "epiphany.role.modeling".to_string(), + linked_subgoal_ids: vec!["phase-6".to_string()], + linked_graph_node_ids: vec!["runtime-spine".to_string()], + instruction: "Model the target before implementation.".to_string(), + launch_document: EpiphanyWorkerLaunchDocument::Role( + crate::EpiphanyRoleWorkerLaunchDocument { + thread_id: "thread-1".to_string(), + role_id: "modeling".to_string(), + state_revision: 7, + objective: Some("keep state typed".to_string()), + active_subgoal_id: None, + active_subgoals: Vec::new(), + active_graph_node_ids: vec!["runtime-spine".to_string()], + investigation_checkpoint: None, + scratch: None, + invariants: Vec::new(), + graphs: None, + recent_evidence: Vec::new(), + recent_observations: Vec::new(), + graph_frontier: None, + graph_checkpoint: None, + planning: None, + churn: None, + }, + ), + output_contract_id: "epiphany.worker.role_result.v0".to_string(), + max_runtime_seconds: Some(60), + runtime_job_id: "turn-1".to_string(), + }, + ) + .expect("launch planning should build binding and runtime link"); + + assert_eq!( + launch_plan.binding.authority_scope.as_deref(), + Some("epiphany.role.modeling") + ); + assert_eq!( + launch_plan.runtime_link.id, + "runtime-link-modeling-checkpoint-worker-turn-1" + ); + assert_eq!(launch_plan.runtime_link.runtime_job_id, "turn-1"); + assert_eq!(launch_plan.runtime_link.runtime_result_id, None); + assert_eq!(launch_plan.runtime_link.role_id, "epiphany-modeler"); + } + #[test] fn runtime_spine_emits_cultnet_hello_frame() -> Result<()> { let temp = tempdir()?; diff --git a/epiphany-core/src/state_update.rs b/epiphany-core/src/state_update.rs index 7f4e7f13..358f8da0 100644 --- a/epiphany-core/src/state_update.rs +++ b/epiphany-core/src/state_update.rs @@ -231,6 +231,479 @@ pub fn apply_epiphany_state_update( state.last_updated_turn_id = reference_turn_id; } +#[cfg(test)] +mod tests { + use super::*; + use epiphany_state_model::EpiphanyBacklogItem; + use epiphany_state_model::EpiphanyGraph; + use epiphany_state_model::EpiphanyGraphNode; + use epiphany_state_model::EpiphanyJobKind; + use epiphany_state_model::EpiphanyObjectiveDraft; + use epiphany_state_model::EpiphanyPlanningCapture; + use epiphany_state_model::EpiphanyPlanningPriority; + use epiphany_state_model::EpiphanyPlanningSourceRef; + use epiphany_state_model::EpiphanyRoadmapStream; + + fn evidence(id: &str) -> EpiphanyEvidenceRecord { + EpiphanyEvidenceRecord { + id: id.to_string(), + kind: "verification".to_string(), + status: "ok".to_string(), + summary: "Evidence summary".to_string(), + code_refs: Vec::new(), + } + } + + fn observation(id: &str, evidence_ids: Vec<&str>) -> EpiphanyObservation { + EpiphanyObservation { + id: id.to_string(), + summary: "Observation summary".to_string(), + source_kind: "smoke".to_string(), + status: "ok".to_string(), + code_refs: Vec::new(), + evidence_ids: evidence_ids.into_iter().map(str::to_string).collect(), + } + } + + fn job_binding(id: &str) -> EpiphanyJobBinding { + EpiphanyJobBinding { + id: id.to_string(), + kind: EpiphanyJobKind::Specialist, + scope: "role-scoped specialist work".to_string(), + owner_role: "epiphany-harness".to_string(), + authority_scope: Some("epiphany.specialist".to_string()), + linked_subgoal_ids: vec!["phase-6".to_string()], + linked_graph_node_ids: vec!["job-surface".to_string()], + blocking_reason: None, + } + } + + fn acceptance_receipt( + id: &str, + result_id: &str, + evidence_id: &str, + ) -> EpiphanyAcceptanceReceipt { + EpiphanyAcceptanceReceipt { + id: id.to_string(), + result_id: result_id.to_string(), + job_id: "runtime-job-1".to_string(), + binding_id: "modeling".to_string(), + surface: "roleAccept".to_string(), + role_id: "modeling".to_string(), + status: "accepted".to_string(), + accepted_at: "2026-05-12T00:00:00Z".to_string(), + accepted_observation_id: Some("obs-modeling".to_string()), + accepted_evidence_id: Some(evidence_id.to_string()), + summary: Some("Accepted modeling result.".to_string()), + } + } + + #[test] + fn state_update_replaces_typed_fields_and_prepends_evidence() { + let mut state = EpiphanyThreadState { + revision: 3, + recent_evidence: vec![EpiphanyEvidenceRecord { + id: "old-evidence".to_string(), + kind: "research".to_string(), + status: "ok".to_string(), + summary: "Older finding".to_string(), + code_refs: Vec::new(), + }], + ..Default::default() + }; + + apply_epiphany_state_update( + &mut state, + EpiphanyStateUpdate { + objective: Some("Keep the map honest".to_string()), + investigation_checkpoint: Some(EpiphanyInvestigationCheckpoint { + checkpoint_id: "ix-1".to_string(), + kind: "slice_planning".to_string(), + focus: "Keep the durable packet small and explicit.".to_string(), + next_action: Some( + "Resume from the packet instead of the ghost transcript.".to_string(), + ), + ..Default::default() + }), + evidence: vec![EpiphanyEvidenceRecord { + id: "new-evidence".to_string(), + kind: "verification".to_string(), + status: "ok".to_string(), + summary: "New finding".to_string(), + code_refs: Vec::new(), + }], + churn: Some(EpiphanyChurnState { + understanding_status: "grounded".to_string(), + diff_pressure: "low".to_string(), + ..Default::default() + }), + ..Default::default() + }, + Some("turn-1".to_string()), + ); + + assert_eq!(state.revision, 4); + assert_eq!(state.objective.as_deref(), Some("Keep the map honest")); + assert_eq!(state.last_updated_turn_id.as_deref(), Some("turn-1")); + assert_eq!( + state + .investigation_checkpoint + .as_ref() + .map(|checkpoint| checkpoint.checkpoint_id.as_str()), + Some("ix-1") + ); + assert_eq!(state.recent_evidence[0].id, "new-evidence"); + assert_eq!(state.recent_evidence[1].id, "old-evidence"); + assert_eq!( + state + .churn + .as_ref() + .map(|churn| churn.diff_pressure.as_str()), + Some("low") + ); + } + + #[test] + fn state_update_replaces_job_bindings_and_prepends_acceptance_receipts() { + let mut state = EpiphanyThreadState { + revision: 2, + recent_evidence: vec![evidence("ev-new")], + job_bindings: vec![job_binding("old")], + acceptance_receipts: vec![acceptance_receipt("accept-old", "result-old", "ev-old")], + ..Default::default() + }; + + apply_epiphany_state_update( + &mut state, + EpiphanyStateUpdate { + job_bindings: Some(vec![job_binding("new")]), + acceptance_receipts: vec![acceptance_receipt("accept-new", "result-new", "ev-new")], + ..Default::default() + }, + Some("turn-jobs".to_string()), + ); + + assert_eq!(state.revision, 3); + assert_eq!(state.job_bindings.len(), 1); + assert_eq!(state.job_bindings[0].id, "new"); + assert_eq!( + state.job_bindings[0].authority_scope.as_deref(), + Some("epiphany.specialist") + ); + assert_eq!(state.acceptance_receipts[0].id, "accept-new"); + assert_eq!(state.acceptance_receipts[1].id, "accept-old"); + } + + #[test] + fn state_update_validation_accepts_and_rejects_planning_state() { + let valid = EpiphanyStateUpdate { + planning: Some(EpiphanyPlanningState { + captures: vec![EpiphanyPlanningCapture { + id: "capture-github-42".to_string(), + title: "Import issue backlog".to_string(), + confidence: "medium".to_string(), + status: "new".to_string(), + source: EpiphanyPlanningSourceRef { + kind: "github_issue".to_string(), + provider: Some("github".to_string()), + repo: Some("GameCult/Epiphany".to_string()), + issue_number: Some(42), + ..Default::default() + }, + ..Default::default() + }], + backlog_items: vec![EpiphanyBacklogItem { + id: "backlog-planning-api".to_string(), + title: "Expose planning projection".to_string(), + kind: "feature".to_string(), + summary: "Make planning state queryable by the GUI.".to_string(), + status: "ready".to_string(), + horizon: "now".to_string(), + priority: EpiphanyPlanningPriority { + value: "p1".to_string(), + rationale: "Unblocks planning operations.".to_string(), + ..Default::default() + }, + confidence: "high".to_string(), + product_area: "gui".to_string(), + lane_hints: vec!["imagination".to_string()], + ..Default::default() + }], + roadmap_streams: vec![EpiphanyRoadmapStream { + id: "stream-gui".to_string(), + title: "GUI Operator Surface".to_string(), + purpose: "Let the human inspect and steer planning.".to_string(), + status: "active".to_string(), + item_ids: vec!["backlog-planning-api".to_string()], + ..Default::default() + }], + objective_drafts: vec![EpiphanyObjectiveDraft { + id: "objdraft-planning-api".to_string(), + title: "Build planning API slice".to_string(), + summary: "Land typed planning state and read-only projection.".to_string(), + source_item_ids: vec!["backlog-planning-api".to_string()], + acceptance_criteria: vec!["Projection returns planning counts.".to_string()], + status: "draft".to_string(), + ..Default::default() + }], + ..Default::default() + }), + ..Default::default() + }; + assert!( + epiphany_state_update_validation_errors(&EpiphanyThreadState::default(), &valid) + .is_empty() + ); + + let invalid = EpiphanyStateUpdate { + planning: Some(EpiphanyPlanningState { + captures: vec![EpiphanyPlanningCapture { + id: "capture-bad-github".to_string(), + title: "Missing repo issue".to_string(), + confidence: "medium".to_string(), + status: "new".to_string(), + source: EpiphanyPlanningSourceRef { + kind: "github_issue".to_string(), + issue_number: None, + ..Default::default() + }, + ..Default::default() + }], + backlog_items: vec![EpiphanyBacklogItem { + id: "backlog-1".to_string(), + title: "Backlog without priority rationale".to_string(), + kind: "feature".to_string(), + summary: "Invalid on purpose.".to_string(), + status: "ready".to_string(), + horizon: "now".to_string(), + priority: EpiphanyPlanningPriority { + value: "p1".to_string(), + rationale: String::new(), + ..Default::default() + }, + confidence: "high".to_string(), + product_area: "gui".to_string(), + ..Default::default() + }], + roadmap_streams: vec![EpiphanyRoadmapStream { + id: "stream-gui".to_string(), + title: "GUI Operator Surface".to_string(), + purpose: "Let the human inspect and steer planning.".to_string(), + status: "active".to_string(), + item_ids: vec!["missing-backlog".to_string()], + ..Default::default() + }], + objective_drafts: vec![EpiphanyObjectiveDraft { + id: "objdraft-empty".to_string(), + title: "Empty acceptance draft".to_string(), + summary: "Invalid on purpose.".to_string(), + source_item_ids: vec!["missing-backlog".to_string()], + status: "draft".to_string(), + ..Default::default() + }], + ..Default::default() + }), + ..Default::default() + }; + + let errors = + epiphany_state_update_validation_errors(&EpiphanyThreadState::default(), &invalid); + assert!( + errors + .iter() + .any(|error| error.contains("repo is required for github_issue")) + ); + assert!( + errors + .iter() + .any(|error| error.contains("priority.rationale")) + ); + assert!( + errors + .iter() + .any(|error| error.contains("references missing backlog item")) + ); + assert!( + errors + .iter() + .any(|error| error.contains("must include at least one acceptance criterion")) + ); + } + + #[test] + fn state_update_validation_rejects_bad_evidence_observations_acceptance_and_bindings() { + let state = EpiphanyThreadState { + observations: vec![observation("obs-existing", vec!["ev-existing"])], + recent_evidence: vec![evidence("ev-existing"), evidence("ev-new")], + acceptance_receipts: vec![acceptance_receipt( + "accept-existing", + "result-existing", + "ev-existing", + )], + ..Default::default() + }; + let update = EpiphanyStateUpdate { + observations: vec![ + observation("obs-existing", vec!["ev-new"]), + observation("obs-dup", vec!["ev-new"]), + observation("obs-dup", vec!["ev-missing"]), + EpiphanyObservation { + id: String::new(), + ..observation("unused", vec!["ev-new"]) + }, + ], + evidence: vec![ + evidence("ev-existing"), + evidence("ev-new"), + evidence("ev-new"), + EpiphanyEvidenceRecord { + id: String::new(), + ..evidence("unused") + }, + ], + acceptance_receipts: vec![ + acceptance_receipt("accept-new", "result-existing", "ev-new"), + acceptance_receipt("accept-new", "result-new", "ev-new"), + ], + job_bindings: Some(vec![ + job_binding("dup"), + job_binding("dup"), + EpiphanyJobBinding { + id: String::new(), + kind: EpiphanyJobKind::Verification, + scope: String::new(), + owner_role: String::new(), + authority_scope: Some(String::new()), + linked_subgoal_ids: Vec::new(), + linked_graph_node_ids: Vec::new(), + blocking_reason: Some(String::new()), + }, + ]), + ..Default::default() + }; + + let errors = epiphany_state_update_validation_errors(&state, &update); + assert!( + errors + .iter() + .any(|error| error.contains("evidence id \"ev-existing\" already exists")) + ); + assert!( + errors + .iter() + .any(|error| error.contains("duplicate evidence id \"ev-new\"")) + ); + assert!( + errors + .iter() + .any(|error| error.contains("observation id \"obs-existing\" already exists")) + ); + assert!( + errors + .iter() + .any(|error| error.contains("duplicate observation id \"obs-dup\"")) + ); + assert!( + errors + .iter() + .any(|error| error.contains("cites missing evidence id")) + ); + assert!( + errors + .iter() + .any(|error| error.contains("runtime result \"result-existing\" already has")) + ); + assert!( + errors + .iter() + .any(|error| error.contains("acceptance receipt id \"accept-new\" is duplicated")) + ); + assert!( + errors + .iter() + .any(|error| error.contains("duplicate job binding id")) + ); + assert!( + errors + .iter() + .any(|error| error.contains("job_binding.id must not be empty")) + ); + assert!( + errors + .iter() + .any(|error| error.contains("job_binding.scope must not be empty")) + ); + assert!( + errors + .iter() + .any(|error| error.contains("job_binding.owner_role must not be empty")) + ); + assert!( + errors + .iter() + .any(|error| error.contains("job_binding.authority_scope must not be empty")) + ); + assert!( + errors + .iter() + .any(|error| error.contains("job_binding.blocking_reason must not be empty")) + ); + } + + #[test] + fn state_update_validation_rejects_structural_replacements_and_checkpoint_evidence_gap() { + let state = EpiphanyThreadState { + graphs: EpiphanyGraphs { + architecture: EpiphanyGraph { + nodes: vec![EpiphanyGraphNode { + id: "state".to_string(), + title: "State".to_string(), + purpose: "Carry the explicit map".to_string(), + ..Default::default() + }], + edges: Vec::new(), + }, + ..Default::default() + }, + ..Default::default() + }; + let frontier_update = EpiphanyStateUpdate { + graph_frontier: Some(EpiphanyGraphFrontier { + active_node_ids: vec!["missing".to_string()], + ..Default::default() + }), + ..Default::default() + }; + let errors = epiphany_state_update_validation_errors(&state, &frontier_update); + assert!( + errors + .iter() + .any(|error| error.contains("graph frontier references missing node")) + ); + + let checkpoint_update = EpiphanyStateUpdate { + investigation_checkpoint: Some(EpiphanyInvestigationCheckpoint { + checkpoint_id: "ix-missing".to_string(), + kind: "source_gathering".to_string(), + focus: "Trace the compaction seam.".to_string(), + next_action: Some("Re-gather source before implementation.".to_string()), + evidence_ids: vec!["ev-missing".to_string()], + ..Default::default() + }), + ..Default::default() + }; + let errors = epiphany_state_update_validation_errors( + &EpiphanyThreadState::default(), + &checkpoint_update, + ); + assert!( + errors + .iter() + .any(|error| error.contains("investigation checkpoint cites missing evidence id")) + ); + } +} + fn validate_epiphany_runtime_links( existing: &[EpiphanyRuntimeLink], links: &[EpiphanyRuntimeLink], diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 2e133c73..b1a5ee66 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -40,6 +40,7 @@ remembers doctrine; the branch remembers the blade. - Agent self-memory review/application now has typed `AgentSelfPatch` core APIs. JSON-value selfPatch functions are compatibility wrappers for CLI/artifact ingress; native callers should parse once at the edge and then call `review_agent_self_patch_document` / `apply_agent_self_patch_document`. - `epiphany-openai-codex-spine` no longer uses Codex's Responses client, model-provider layer, `codex-api`, `codex-model-provider`, `codex-model-provider-info`, or `codex-protocol` for outbound model requests. It reaches `codex-login` only through `epiphany-openai-auth-spine` so credential loading/refresh stays Codex-compatible while request/status/event/runtime surfaces stay typed Epiphany documents. - Durable Epiphany state types now live in root crate `epiphany-state-model`. `codex-protocol` re-exports them only so old rollout/API callers keep compiling; do not add new durable `Epiphany*` state structs under `vendor/codex`. `epiphany-core` imports the native model directly and no longer depends on `codex-protocol`. Pressure derives from native `EpiphanyTokenUsageSnapshot`; Codex token telemetry is mapped at the bridge. Codex rollout/event reconstruction now lives in `codex-core::epiphany_rollout`, where the Codex `RolloutItem` / `EventMsg` types belong. Exact retrieval path matching is native to `epiphany-core`, so `epiphany-core` has no direct vendored Codex crate dependency. +- State-update contract proof now lives in native `epiphany-core`. The old giant `codex_thread.rs` Epiphany state-update test fixture was cut; vendored `CodexThread` may call the native mutation law for compatibility, but it must not regain ownership of state-update semantics or heartbeat launch-plan policy. - The May 2026 foundation migration in `notes/epiphany-ideal-architecture-rebuild-plan.md` is complete enough to defend. Read `notes/epiphany-architectural-teardown.md` before touching Epiphany control-plane code, but do not treat it as an open-ended excuse to keep shaving the altar. The rebuild moved scene projection, freshness derivation, pressure policy, targeted context shards, bounded graph traversal, job/progress view derivation, planning view derivation, reorientation resume/regather verdict policy, pure CRRC recommendation policy, fixed-lane coordinator decision policy, role-board projection policy, role/reorient result interpretation, role self-persistence review policy, and role/reorient acceptance bundle policy into `epiphany-core`; typed acceptance receipts replaced summary-string identity for live accept paths, runtime links are dual-written on launch, and result read-back now prefers runtime links. - Read `notes/epiphany-ideal-architecture-rebuild-plan.md` immediately after the teardown. It defines the smallest coherent replacement architecture: durable `EpiphanyState`, runtime-spine-owned `RuntimeState`, separate role memory/heartbeat state, `epiphany-core` as policy owner, app-server as adapter, Aquarium as reflector, typed acceptance receipts, and derived view lenses instead of a protocol verb zoo. - Do not continue Aquarium UI, bridge, Face, or dogfood expansion until the teardown has a source-grounded cleanup slice plan. Epiphany is the foundation; patches on patches are not a purification rite, they are how the altar becomes load-bearing garbage. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 3da15b573c55a1ec91a63d651702fae1481184fe..ce5da31de96601ee472957b46308695937e6bc7a 100644 GIT binary patch delta 252 zcmY+ivbY?z46{ilmt7$Cy0&RxOd_%tUJr>B$`S=dj-K*2!e&Z zAc&C0UQFW)_!gG_bUwg2@O|AjF76tai*vKx$E9myw$7&Jd1}nVTl4&0^GmyIZxm&x zs5bjmx7Vq9>sOyQE9PTT?wEyTj5Rny(S1UNfWv5Akpf1r997UNC}qu)_gB+AQ743T zOEUPV3=!3j2q^jFFpdk$LaPu6M?N0FHrI6A|0@79IL-=*B>SusEv*TMz(a*hcnE^( eNI-u+kT`fdh9t}b;G;;=I(_Q?E_r3wOvRZ=x delta 49 zcmeC5&;DZ$`vhgCb9Nh5r!g{GH}7HGzK4-<*%`5hEk*`LX1WHZx`yTwCRWCV FQ2@ge5A*;4 diff --git a/state/map.yaml b/state/map.yaml index 528a6445..60ff344d 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -286,10 +286,9 @@ rejected_paths: current_status: summary: The Codex starvation pass has five typing receipts, fifty-two processor/foundation cuts, a real physical extraction in `epiphany-codex-bridge`, a root `epiphany-state-model` crate for durable Epiphany state, a source-grounded Codex auth spine inventory, the native `epiphany-openai-adapter` document boundary, a buildable Codex auth/model wrapper in `epiphany-openai-codex-spine`, the first native runtime caller in `epiphany-openai-runtime`, native heartbeat launch planning/job opening in `epiphany-core`, native state-update contract/validation/application in `epiphany-core`, native exact retrieval path matching in `epiphany-core`, and the route-facing mutation/view/coordinator projection services in `epiphany-codex-bridge`. `codex-protocol` no longer defines the durable `Epiphany*` state structs; it re-exports the native model for legacy rollout/API compatibility while `epiphany-core` imports the native model directly. `epiphany-core` now has no direct vendored Codex dependency: pressure derivation accepts native `EpiphanyTokenUsageSnapshot`, Codex rollout replay moved to `codex-core::epiphany_rollout`, and exact retrieval search uses Epiphany-owned workspace walking/path scoring instead of `codex-file-search`. The bridge owns Codex-facing Epiphany adapters for scene, jobs, context/planning/graph query, retrieval/indexing projection, result projection, runtime-spine result adaptation, launch/prompt doctrine, mutation document mechanics, mutation service update/promote/role-accept/reorient-accept/role-launch/reorient-launch/job-launch/job-interrupt application, live-state projection, pressure/pre-compaction rendering, freshness/reorientation projection, CRRC/coordinator/role-board projection, watcher invalidation adaptation, full `thread/epiphany/view` response construction, freshness/context/graph-query response construction, role/reorient result response construction, distill/propose patch-response construction, coordinator status/view construction, and acceptance/evidence signal helpers. Vendored app-server no longer retains root Epiphany modules; `codex_message_processor.rs` still contains route dispatch, JSON-RPC response shaping, watcher registration/snapshotting calls, and notification emission, but its root no longer imports the evacuated Epiphany policy/protocol registry or owns the watcher invalidation manager. Codex product surfaces for apps, skills, plugins, marketplace, and external agent config import are cut from the live app-server/protocol path: list routes return empty lists only where compatibility shells still exist, mutation/detail routes report disabled where they survive, `externalAgentConfig/*` routes and import-completed notification are gone, helper/import modules and stale implementation-shape tests were deleted, runtime feature enablement no longer supports `apps` or `plugins`, config writes no longer emit plugin-toggle analytics, plugin/skill cache invalidation on config writes is gone, plugin startup warmups and app-list refreshes were removed, and app-server no longer directly depends on `codex-plugin`, `codex-chatgpt`, or `codex-core-plugins`. `Config::to_mcp_config` now keeps user-declared MCP servers but no longer accepts `PluginsManager`, folds plugin MCP servers into the runtime, enables Codex apps, enables skill MCP dependency install, or advertises plugin capability summaries; `TurnContext::apps_enabled()` now returns false so model turns cannot awaken Codex Apps through ChatGPT auth or feature flags; session startup and per-turn context no longer load skills, turns receive an empty `SkillLoadOutcome`, internal `Op::ListSkills` is gone from `codex-protocol` / `codex-core` / TUI command routing, the Codex skill watcher has been collapsed to a no-op marker with no file watcher, skill-root registration, cache-clear task, or `SkillsUpdateAvailable` session listener, model turns no longer run skill mention collection, skill env-var prompts, skill MCP dependency installation, skill instruction injection, or implicit skill invocation analytics from shell tools, and the turn prompt/router path no longer loads plugin/app capability summaries, injects available-skill/available-plugin/plugin instruction fragments, parses plugin/app mentions, records app/plugin invocation telemetry, or filters MCP tools through Codex app connectors. Skill mutation/list/notification is gone from the app-server edge too: `skills/list`, `skills/config/write`, `skills/changed`, `Op::ListSkills`, `EventMsg::ListSkillsResponse`, `EventMsg::SkillsUpdateAvailable`, the app-server handlers, TUI slash/startup/list/manage-skill surfaces, generated schema entries, core skill-list suite, and fake live-reload/developer-message tests were deleted. Skill mention plumbing is also gone from the live TUI/protocol edge: no `UserInput::Skill` variant, app-server schema branch, TUI skill popup, `skills_all` cache, composer `set_skill_mentions`, skill mention insertion, `skill://` / `SKILL.md` history decoding, or stale skill popup snapshot remains. The `codex-core-skills` crate, core `skills` re-export shim, no-op `skills_watcher`, session `SkillsManager` service, and per-turn `TurnSkillsContext` are deleted; core/protocol no longer keep legacy `` or `` prompt compatibility, memory usage classification, snapshot normalization, or cleanup paths. Remaining product-shaped skill residue is also cut from memory templates, app-server docs/tests/warnings, TUI tooltips/placeholders/warnings/comments, app-server v1 docs, agent-tool schema text, stale `UserInput::Skill` test arms, and obsolete feature flags/schema entries. App-instruction prompt compatibility is cut too: no `APPS_INSTRUCTIONS` protocol constants, no `` snapshot canonicalization, and MCP fixture tests now use neutral `demo_mcp` / `calendar_mcp` names instead of `codex_apps` / `/api/codex/apps` branding. The TUI app connector product path is also deleted: no `/apps` slash command, app-link view, connector AppEvent variants, app connector cache/popup/refresh loop, app mention insertion, `app://` mention codec support, stale app-popup tests, or TUI browser-opening shim remains. The `app/list` route and `app/list/updated` notification are gone from app-server protocol, generated schemas, app-server dispatch, test helpers, and docs; `AppInfo`/app-list metadata schema structs are deleted; the orphaned `codex-connectors` crate and `core::connectors` shim are deleted from the workspace; Codex app analytics no longer tracks `codex_app_mentioned` / `codex_app_used` facts, events, reducer arms, client APIs, or dedupe cache state; `[apps.*]`, `Feature::Apps`, `include_apps_instructions`, app config/read schemas, app managed requirements, app prompt tests, and app file-MCP tests are gone; `Feature::Plugins`, `Feature::RemotePlugin`, `plugin://` structured mentions, plugin instruction tags, and stale plugin popup snapshots are gone too; `[plugins]` / `[marketplaces]`, `PluginConfig`, `MarketplaceConfig`, marketplace edit helpers, stale plugin/external-agent README route claims, and generated schema entries are removed from config; and the bundled `codex-skills` sample-skill crate plus embedded imagegen/openai-docs/plugin-creator/skill-creator/skill-installer payloads are deleted from the workspace. The vendored `vendor/codex/sdk` subtree is deleted as non-spine app-server client/release scaffolding, including stale generated Python SDK route types for removed product surfaces; the root pnpm workspace no longer lists `sdk/typescript`. Live MCP tool approval is named `McpToolApproval`, not `AppToolApproval`, and remains under MCP server config; live structured user-input mentions are MCP-shaped only. `codex_message_processor.rs` is about 9,482 lines; root `message_processor.rs` is about 1,292 lines after the external-agent/config-product cuts; `epiphany_read_routes.rs` is about 579 lines after the full view and read-response extractions; `epiphany_mutation_routes.rs` is about 694 lines after route-load/notification helper cuts and product-surface starvation; the old vendored `epiphany_state_helpers.rs` rollout-state loader is deleted and its remaining function lives in `epiphany-codex-bridge::state`; and `codex_thread.rs` is about 1,425 lines after state-update law moved native. `epiphany-openai-adapter` defines CultCache `DatabaseEntry` request/event/receipt/auth-status documents with no Codex app-server or generic JSON dependency. `epiphany-openai-codex-spine` owns the native OpenAI model transport path: it exposes `default_codex_home` and `auth_manager`, projects `epiphany-openai-auth-spine` `AuthManager` / `CodexAuth` into typed `EpiphanyOpenAiAdapterStatus`, builds a local serializable OpenAI Responses request body instead of Codex protocol request/item/config types, opens HTTP Responses streams through `codex-client`, parses Responses SSE frames into typed stream events and receipts, and no longer directly or transitively depends on Codex's Responses client, model-provider stack, `codex-protocol`, `codex-api`, `codex-model-provider`, or `codex-model-provider-info`; it intentionally reaches retained vendored `codex-login` only through `epiphany-openai-auth-spine`. Auth identity, credential storage, and ChatGPT token refresh remain owned by the Codex-compatible auth organ; external bearer command auth and agent identity remain retained Codex auth capabilities to expose only if a typed Epiphany surface needs them. `epiphany-openai-runtime` no longer depends directly on `codex-login`; it writes OpenAI status/request/stream-event/receipt records plus runtime sessions/jobs/results/events into `epiphany-core::runtime_spine_cache`, can emit the assistant text artifact for legacy result consumers, is now the default executor for startup-only repo birth specialists, and now has `run-worker` to load a durable `EpiphanyRuntimeWorkerLaunchRequest`, call the typed OpenAI runtime through the spine, persist typed `EpiphanyRuntimeRoleWorkerResult` / `EpiphanyRuntimeReorientWorkerResult` documents, and complete the original heartbeat/specialist runtime job without Codex worker execution; `codex exec` survives only as explicit fallback in repo-birth startup. `epiphany-mvp-coordinator` now invokes `epiphany-openai-runtime run-worker` for launched runtime job ids and no longer creates a second coordinator-owned shadow runtime job/result as a compensator. `epiphany-core::plan_runtime_spine_heartbeat_launch` validates launch contracts/conflicts and projects durable job bindings/runtime links; `epiphany-core::open_runtime_spine_heartbeat_job` owns the initialize-runtime/ensure-session/create-job sequence and now persists `EpiphanyRuntimeWorkerLaunchRequest` CultCache records keyed by runtime job id, with indexed kind/contract fields plus MessagePack-encoded typed launch documents instead of discarding work intent after queue receipt creation; `roleResult` and `reorientResult` now require typed worker result documents; generic job summaries are lifecycle receipts, not reviewable findings; `epiphany-core::EpiphanyStateUpdate`, `epiphany_state_update_validation_errors`, and `apply_epiphany_state_update` own state-update document shape and mutation law while `codex-core` calls them for compatibility. Vendored `CodexThread::epiphany_launch_job` is now mostly revision check, persistence validation, launch request handoff, and rollout/session writeback for the compatibility route. Vendored `codex-core` no longer has `epiphany_distillation`, `epiphany_promotion`, `epiphany_proposal`, or `epiphany_retrieval` husk modules; it re-exports native surfaces from `epiphany-core` directly, and now owns only the real Codex rollout adapter because it is a host seam. `epiphany-runtime-spine` advertises OpenAI adapter status/model-request/stream-event/receipt CultNet schemas and mutation contracts, so the native caller now has a real storage route instead of only JSON-RPC. MCP itself is correctly JSON at its protocol edge; the target is typed Epiphany intent/result/receipt wrappers around MCP, not replacing MCP JSON. The current OpenAI request path no longer pulls `codex-api`; `codex-login` remains deliberately sealed behind `epiphany-openai-auth-spine`, and the next spine decision is another Epiphany-in-vendor evacuation surface unless a concrete typed auth-status/runtime need requires exposing external bearer or agent-identity auth. latest_cut: > - Retrieval query input law moved from the vendored - `thread/epiphany/retrieve` route into - `epiphany-core::normalize_epiphany_retrieve_query`. App-server still maps - native validation failures to JSON-RPC invalid-request responses, but query - trimming, empty-query rejection, zero-limit rejection, and limit clamping - are now native Epiphany retrieval policy. + Vendored `codex_thread.rs` shed its giant Epiphany state-update contract + test fixture. `epiphany-core` now owns the proof for + `EpiphanyStateUpdate` validation/application and heartbeat launch-plan + lifecycle boundaries, while vendored `CodexThread` remains a compatibility + host seam that calls native policy instead of proving or owning it. next_action: Continue the Codex starvation campaign at the next Epiphany-in-vendor evacuation surface; keep vendored Codex code as host routing/lifecycle glue only, and do not cut the retained `codex-login` auth organ unless replacing it with an explicitly permitted OpenAI path rather than a lookalike. diff --git a/vendor/codex/codex-rs/core/src/codex_thread.rs b/vendor/codex/codex-rs/core/src/codex_thread.rs index 9b3f336f..fc93f410 100644 --- a/vendor/codex/codex-rs/core/src/codex_thread.rs +++ b/vendor/codex/codex-rs/core/src/codex_thread.rs @@ -20,24 +20,8 @@ use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; -#[cfg(test)] -use codex_protocol::protocol::EpiphanyAcceptanceReceipt; -#[cfg(test)] -use codex_protocol::protocol::EpiphanyChurnState; -#[cfg(test)] -use codex_protocol::protocol::EpiphanyEvidenceRecord; -#[cfg(test)] -use codex_protocol::protocol::EpiphanyGraphFrontier; -#[cfg(test)] -use codex_protocol::protocol::EpiphanyGraphs; -#[cfg(test)] -use codex_protocol::protocol::EpiphanyInvestigationCheckpoint; use codex_protocol::protocol::EpiphanyJobBinding; use codex_protocol::protocol::EpiphanyJobKind; -#[cfg(test)] -use codex_protocol::protocol::EpiphanyObservation; -#[cfg(test)] -use codex_protocol::protocol::EpiphanyPlanningState; use codex_protocol::protocol::EpiphanyRetrievalState; use codex_protocol::protocol::EpiphanyStateItem; use codex_protocol::protocol::EpiphanyThreadState; @@ -784,636 +768,6 @@ fn clear_epiphany_job_binding_backend( bindings } -#[cfg(test)] -mod epiphany_update_tests { - use super::*; - use codex_protocol::protocol::EpiphanyBacklogItem; - use codex_protocol::protocol::EpiphanyGraph; - use codex_protocol::protocol::EpiphanyGraphNode; - use codex_protocol::protocol::EpiphanyJobKind; - use codex_protocol::protocol::EpiphanyPlanningCapture; - use codex_protocol::protocol::EpiphanyPlanningPriority; - use codex_protocol::protocol::EpiphanyPlanningSourceRef; - - fn evidence(id: &str) -> EpiphanyEvidenceRecord { - EpiphanyEvidenceRecord { - id: id.to_string(), - kind: "verification".to_string(), - status: "ok".to_string(), - summary: "Evidence summary".to_string(), - code_refs: Vec::new(), - } - } - - fn observation(id: &str, evidence_ids: Vec<&str>) -> EpiphanyObservation { - EpiphanyObservation { - id: id.to_string(), - summary: "Observation summary".to_string(), - source_kind: "smoke".to_string(), - status: "ok".to_string(), - code_refs: Vec::new(), - evidence_ids: evidence_ids.into_iter().map(str::to_string).collect(), - } - } - - fn job_binding(id: &str) -> EpiphanyJobBinding { - EpiphanyJobBinding { - id: id.to_string(), - kind: EpiphanyJobKind::Specialist, - scope: "role-scoped specialist work".to_string(), - owner_role: "epiphany-harness".to_string(), - authority_scope: Some("epiphany.specialist".to_string()), - linked_subgoal_ids: vec!["phase-6".to_string()], - linked_graph_node_ids: vec!["job-surface".to_string()], - blocking_reason: None, - } - } - - fn acceptance_receipt( - id: &str, - result_id: &str, - evidence_id: &str, - ) -> EpiphanyAcceptanceReceipt { - EpiphanyAcceptanceReceipt { - id: id.to_string(), - result_id: result_id.to_string(), - job_id: "runtime-job-1".to_string(), - binding_id: "modeling".to_string(), - surface: "roleAccept".to_string(), - role_id: "modeling".to_string(), - status: "accepted".to_string(), - accepted_at: "2026-05-12T00:00:00Z".to_string(), - accepted_observation_id: Some("obs-modeling".to_string()), - accepted_evidence_id: Some(evidence_id.to_string()), - summary: Some("Accepted modeling result.".to_string()), - } - } - - #[test] - fn apply_epiphany_state_update_replaces_typed_fields_and_prepends_evidence() { - let mut state = EpiphanyThreadState { - revision: 3, - recent_evidence: vec![EpiphanyEvidenceRecord { - id: "old-evidence".to_string(), - kind: "research".to_string(), - status: "ok".to_string(), - summary: "Older finding".to_string(), - code_refs: Vec::new(), - }], - ..Default::default() - }; - - apply_epiphany_state_update( - &mut state, - EpiphanyStateUpdate { - objective: Some("Keep the map honest".to_string()), - investigation_checkpoint: Some(EpiphanyInvestigationCheckpoint { - checkpoint_id: "ix-1".to_string(), - kind: "slice_planning".to_string(), - focus: "Keep the durable packet small and explicit.".to_string(), - next_action: Some( - "Resume from the packet instead of the ghost transcript.".to_string(), - ), - ..Default::default() - }), - evidence: vec![EpiphanyEvidenceRecord { - id: "new-evidence".to_string(), - kind: "verification".to_string(), - status: "ok".to_string(), - summary: "New finding".to_string(), - code_refs: Vec::new(), - }], - churn: Some(EpiphanyChurnState { - understanding_status: "grounded".to_string(), - diff_pressure: "low".to_string(), - ..Default::default() - }), - ..Default::default() - }, - Some("turn-1".to_string()), - ); - - assert_eq!(state.revision, 4); - assert_eq!(state.objective.as_deref(), Some("Keep the map honest")); - assert_eq!(state.last_updated_turn_id.as_deref(), Some("turn-1")); - assert_eq!( - state - .investigation_checkpoint - .as_ref() - .map(|checkpoint| checkpoint.checkpoint_id.as_str()), - Some("ix-1") - ); - assert_eq!(state.recent_evidence[0].id, "new-evidence"); - assert_eq!(state.recent_evidence[1].id, "old-evidence"); - assert_eq!( - state - .churn - .as_ref() - .map(|churn| churn.diff_pressure.as_str()), - Some("low") - ); - } - - #[test] - fn apply_epiphany_state_update_replaces_job_bindings() { - let mut state = EpiphanyThreadState { - revision: 2, - job_bindings: vec![job_binding("old")], - ..Default::default() - }; - - apply_epiphany_state_update( - &mut state, - EpiphanyStateUpdate { - job_bindings: Some(vec![job_binding("new")]), - ..Default::default() - }, - Some("turn-jobs".to_string()), - ); - - assert_eq!(state.revision, 3); - assert_eq!(state.job_bindings.len(), 1); - assert_eq!(state.job_bindings[0].id, "new"); - assert_eq!( - state.job_bindings[0].authority_scope.as_deref(), - Some("epiphany.specialist") - ); - assert_eq!(state.last_updated_turn_id.as_deref(), Some("turn-jobs")); - } - - #[test] - fn apply_epiphany_state_update_prepends_acceptance_receipts() { - let mut state = EpiphanyThreadState { - revision: 2, - recent_evidence: vec![evidence("ev-new")], - acceptance_receipts: vec![acceptance_receipt("accept-old", "result-old", "ev-old")], - ..Default::default() - }; - - apply_epiphany_state_update( - &mut state, - EpiphanyStateUpdate { - acceptance_receipts: vec![acceptance_receipt("accept-new", "result-new", "ev-new")], - ..Default::default() - }, - Some("turn-accept".to_string()), - ); - - assert_eq!(state.revision, 3); - assert_eq!(state.acceptance_receipts[0].id, "accept-new"); - assert_eq!(state.acceptance_receipts[1].id, "accept-old"); - } - - #[test] - fn validate_epiphany_state_update_accepts_planning_state() { - let update = EpiphanyStateUpdate { - planning: Some(EpiphanyPlanningState { - captures: vec![EpiphanyPlanningCapture { - id: "capture-github-42".to_string(), - title: "Import issue backlog".to_string(), - confidence: "medium".to_string(), - status: "new".to_string(), - source: EpiphanyPlanningSourceRef { - kind: "github_issue".to_string(), - provider: Some("github".to_string()), - repo: Some("GameCult/Epiphany".to_string()), - issue_number: Some(42), - ..Default::default() - }, - ..Default::default() - }], - backlog_items: vec![EpiphanyBacklogItem { - id: "backlog-planning-api".to_string(), - title: "Expose planning projection".to_string(), - kind: "feature".to_string(), - summary: "Make planning state queryable by the GUI.".to_string(), - status: "ready".to_string(), - horizon: "now".to_string(), - priority: EpiphanyPlanningPriority { - value: "p1".to_string(), - rationale: "Unblocks planning operations.".to_string(), - ..Default::default() - }, - confidence: "high".to_string(), - product_area: "gui".to_string(), - lane_hints: vec!["imagination".to_string()], - ..Default::default() - }], - roadmap_streams: vec![codex_protocol::protocol::EpiphanyRoadmapStream { - id: "stream-gui".to_string(), - title: "GUI Operator Surface".to_string(), - purpose: "Let the human inspect and steer planning.".to_string(), - status: "active".to_string(), - item_ids: vec!["backlog-planning-api".to_string()], - ..Default::default() - }], - objective_drafts: vec![codex_protocol::protocol::EpiphanyObjectiveDraft { - id: "objdraft-planning-api".to_string(), - title: "Build planning API slice".to_string(), - summary: "Land typed planning state and read-only projection.".to_string(), - source_item_ids: vec!["backlog-planning-api".to_string()], - acceptance_criteria: vec!["Projection returns planning counts.".to_string()], - status: "draft".to_string(), - ..Default::default() - }], - ..Default::default() - }), - ..Default::default() - }; - - assert!( - epiphany_state_update_validation_errors(&EpiphanyThreadState::default(), &update) - .is_empty() - ); - } - - #[test] - fn validate_epiphany_state_update_rejects_invalid_planning_state() { - let update = EpiphanyStateUpdate { - planning: Some(EpiphanyPlanningState { - captures: vec![EpiphanyPlanningCapture { - id: "capture-bad-github".to_string(), - title: "Missing repo issue".to_string(), - confidence: "medium".to_string(), - status: "new".to_string(), - source: EpiphanyPlanningSourceRef { - kind: "github_issue".to_string(), - issue_number: None, - ..Default::default() - }, - ..Default::default() - }], - backlog_items: vec![EpiphanyBacklogItem { - id: "backlog-1".to_string(), - title: "Backlog without priority rationale".to_string(), - kind: "feature".to_string(), - summary: "Invalid on purpose.".to_string(), - status: "ready".to_string(), - horizon: "now".to_string(), - priority: EpiphanyPlanningPriority { - value: "p1".to_string(), - rationale: String::new(), - ..Default::default() - }, - confidence: "high".to_string(), - product_area: "gui".to_string(), - ..Default::default() - }], - roadmap_streams: vec![codex_protocol::protocol::EpiphanyRoadmapStream { - id: "stream-gui".to_string(), - title: "GUI Operator Surface".to_string(), - purpose: "Let the human inspect and steer planning.".to_string(), - status: "active".to_string(), - item_ids: vec!["missing-backlog".to_string()], - ..Default::default() - }], - objective_drafts: vec![codex_protocol::protocol::EpiphanyObjectiveDraft { - id: "objdraft-empty".to_string(), - title: "Empty acceptance draft".to_string(), - summary: "Invalid on purpose.".to_string(), - source_item_ids: vec!["missing-backlog".to_string()], - status: "draft".to_string(), - ..Default::default() - }], - ..Default::default() - }), - ..Default::default() - }; - - let errors = - epiphany_state_update_validation_errors(&EpiphanyThreadState::default(), &update); - assert!( - errors - .iter() - .any(|error| error.contains("repo is required for github_issue")) - ); - assert!( - errors - .iter() - .any(|error| error.contains("priority.rationale")) - ); - assert!( - errors - .iter() - .any(|error| error.contains("references missing backlog item")) - ); - assert!( - errors - .iter() - .any(|error| error.contains("must include at least one acceptance criterion")) - ); - } - - #[test] - fn validate_epiphany_state_update_accepts_observation_linked_to_existing_evidence() { - let state = EpiphanyThreadState { - recent_evidence: vec![evidence("ev-existing")], - ..Default::default() - }; - let update = EpiphanyStateUpdate { - observations: vec![observation("obs-new", vec!["ev-existing"])], - ..Default::default() - }; - - assert!(epiphany_state_update_validation_errors(&state, &update).is_empty()); - } - - #[test] - fn validate_epiphany_state_update_rejects_observation_with_missing_evidence() { - let update = EpiphanyStateUpdate { - observations: vec![observation("obs-new", vec!["ev-missing"])], - ..Default::default() - }; - - let errors = - epiphany_state_update_validation_errors(&EpiphanyThreadState::default(), &update); - - assert!( - errors - .iter() - .any(|error| error.contains("cites missing evidence id")) - ); - } - - #[test] - fn validate_epiphany_state_update_rejects_empty_and_duplicate_append_ids() { - let state = EpiphanyThreadState { - observations: vec![observation("obs-existing", vec!["ev-existing"])], - recent_evidence: vec![evidence("ev-existing")], - ..Default::default() - }; - let update = EpiphanyStateUpdate { - observations: vec![ - observation("obs-existing", vec!["ev-new"]), - observation("obs-dup", vec!["ev-new"]), - observation("obs-dup", vec!["ev-new"]), - EpiphanyObservation { - id: String::new(), - ..observation("unused", vec!["ev-new"]) - }, - ], - evidence: vec![ - evidence("ev-existing"), - evidence("ev-new"), - evidence("ev-new"), - EpiphanyEvidenceRecord { - id: String::new(), - ..evidence("unused") - }, - ], - ..Default::default() - }; - - let errors = epiphany_state_update_validation_errors(&state, &update); - - assert!( - errors - .iter() - .any(|error| error.contains("evidence id \"ev-existing\" already exists")) - ); - assert!( - errors - .iter() - .any(|error| error.contains("duplicate evidence id \"ev-new\"")) - ); - assert!( - errors - .iter() - .any(|error| error.contains("observation id \"obs-existing\" already exists")) - ); - assert!( - errors - .iter() - .any(|error| error.contains("duplicate observation id \"obs-dup\"")) - ); - assert!( - errors - .iter() - .any(|error| error.contains("patch.evidence.id must not be empty")) - ); - assert!( - errors - .iter() - .any(|error| error.contains("patch.observations.id must not be empty")) - ); - } - - #[test] - fn validate_epiphany_state_update_rejects_duplicate_acceptance_result() { - let state = EpiphanyThreadState { - acceptance_receipts: vec![acceptance_receipt( - "accept-existing", - "result-existing", - "ev-existing", - )], - recent_evidence: vec![evidence("ev-existing"), evidence("ev-new")], - ..Default::default() - }; - let update = EpiphanyStateUpdate { - acceptance_receipts: vec![ - acceptance_receipt("accept-new", "result-existing", "ev-new"), - acceptance_receipt("accept-new", "result-new", "ev-new"), - ], - ..Default::default() - }; - - let errors = epiphany_state_update_validation_errors(&state, &update); - - assert!( - errors - .iter() - .any(|error| error.contains("runtime result \"result-existing\" already has")) - ); - assert!( - errors - .iter() - .any(|error| error.contains("acceptance receipt id \"accept-new\" is duplicated")) - ); - } - - #[test] - fn validate_epiphany_state_update_rejects_structurally_invalid_replacements() { - let state = EpiphanyThreadState { - graphs: EpiphanyGraphs { - architecture: EpiphanyGraph { - nodes: vec![EpiphanyGraphNode { - id: "state".to_string(), - title: "State".to_string(), - purpose: "Carry the explicit map".to_string(), - ..Default::default() - }], - edges: Vec::new(), - }, - ..Default::default() - }, - ..Default::default() - }; - let update = EpiphanyStateUpdate { - graph_frontier: Some(EpiphanyGraphFrontier { - active_node_ids: vec!["missing".to_string()], - ..Default::default() - }), - ..Default::default() - }; - - let errors = epiphany_state_update_validation_errors(&state, &update); - - assert!( - errors - .iter() - .any(|error| error.contains("graph frontier references missing node")) - ); - } - - #[test] - fn validate_epiphany_state_update_rejects_investigation_checkpoint_with_missing_evidence() { - let update = EpiphanyStateUpdate { - investigation_checkpoint: Some(EpiphanyInvestigationCheckpoint { - checkpoint_id: "ix-missing".to_string(), - kind: "source_gathering".to_string(), - focus: "Trace the compaction seam.".to_string(), - next_action: Some("Re-gather source before implementation.".to_string()), - evidence_ids: vec!["ev-missing".to_string()], - ..Default::default() - }), - ..Default::default() - }; - - let errors = - epiphany_state_update_validation_errors(&EpiphanyThreadState::default(), &update); - - assert!( - errors - .iter() - .any(|error| error.contains("investigation checkpoint cites missing evidence id")) - ); - } - - #[test] - fn validate_epiphany_state_update_rejects_invalid_job_bindings() { - let update = EpiphanyStateUpdate { - job_bindings: Some(vec![ - job_binding("dup"), - job_binding("dup"), - EpiphanyJobBinding { - id: String::new(), - kind: EpiphanyJobKind::Verification, - scope: String::new(), - owner_role: String::new(), - authority_scope: Some(String::new()), - linked_subgoal_ids: Vec::new(), - linked_graph_node_ids: Vec::new(), - blocking_reason: Some(String::new()), - }, - ]), - ..Default::default() - }; - - let errors = - epiphany_state_update_validation_errors(&EpiphanyThreadState::default(), &update); - - assert!( - errors - .iter() - .any(|error| error.contains("duplicate job binding id")) - ); - assert!( - errors - .iter() - .any(|error| error.contains("job_binding.id must not be empty")) - ); - assert!( - errors - .iter() - .any(|error| error.contains("job_binding.scope must not be empty")) - ); - assert!( - errors - .iter() - .any(|error| error.contains("job_binding.owner_role must not be empty")) - ); - assert!( - errors - .iter() - .any(|error| error.contains("job_binding.authority_scope must not be empty")) - ); - assert!( - errors - .iter() - .any(|error| error.contains("job_binding.blocking_reason must not be empty")) - ); - } - - #[test] - fn plan_epiphany_job_launch_leaves_lifecycle_to_runtime_links() { - let request = EpiphanyJobLaunchRequest { - expected_revision: Some(7), - binding_id: "modeling-checkpoint-worker".to_string(), - kind: EpiphanyJobKind::Specialist, - scope: "role-scoped modeling/checkpoint maintenance".to_string(), - owner_role: "epiphany-modeler".to_string(), - authority_scope: "epiphany.role.modeling".to_string(), - linked_subgoal_ids: vec!["phase-6".to_string()], - linked_graph_node_ids: vec!["runtime-spine".to_string()], - instruction: "Model the target before implementation.".to_string(), - launch_document: EpiphanyWorkerLaunchDocument::Role( - epiphany_core::EpiphanyRoleWorkerLaunchDocument { - thread_id: "thread-1".to_string(), - role_id: "modeling".to_string(), - state_revision: 7, - objective: Some("keep state typed".to_string()), - active_subgoal_id: None, - active_subgoals: Vec::new(), - active_graph_node_ids: vec!["runtime-spine".to_string()], - investigation_checkpoint: None, - scratch: None, - invariants: Vec::new(), - graphs: None, - recent_evidence: Vec::new(), - recent_observations: Vec::new(), - graph_frontier: None, - graph_checkpoint: None, - planning: None, - churn: None, - }, - ), - output_contract_id: "epiphany.worker.role_result.v0".to_string(), - max_runtime_seconds: Some(60), - }; - - let launch_plan = plan_runtime_spine_heartbeat_launch( - &EpiphanyThreadState::default(), - RuntimeSpineHeartbeatLaunchPlanOptions { - binding_id: request.binding_id, - kind: request.kind, - scope: request.scope, - owner_role: request.owner_role, - authority_scope: request.authority_scope, - linked_subgoal_ids: request.linked_subgoal_ids, - linked_graph_node_ids: request.linked_graph_node_ids, - instruction: request.instruction, - launch_document: request.launch_document, - output_contract_id: request.output_contract_id, - max_runtime_seconds: request.max_runtime_seconds, - runtime_job_id: "turn-1".to_string(), - }, - ) - .expect("launch planning should build binding and runtime link"); - - assert_eq!( - launch_plan.binding.authority_scope.as_deref(), - Some("epiphany.role.modeling") - ); - assert_eq!( - launch_plan.runtime_link.id, - "runtime-link-modeling-checkpoint-worker-turn-1" - ); - assert_eq!(launch_plan.runtime_link.runtime_job_id, "turn-1"); - assert_eq!(launch_plan.runtime_link.runtime_result_id, None); - assert_eq!(launch_plan.runtime_link.role_id, "epiphany-modeler"); - } -} - fn pending_message_input_item(message: &ResponseItem) -> CodexResult { match message { ResponseItem::Message { role, content, .. } => Ok(ResponseInputItem::Message {