diff --git a/epiphany-codex-bridge/src/checkpoint.rs b/epiphany-codex-bridge/src/checkpoint.rs new file mode 100644 index 00000000..f50c6be2 --- /dev/null +++ b/epiphany-codex-bridge/src/checkpoint.rs @@ -0,0 +1,61 @@ +#[derive(Default)] +pub struct EpiphanyCheckpointInterventionState { + last_intervention_turn_id: Option, + pending_compaction_turn_id: Option, +} + +impl EpiphanyCheckpointInterventionState { + pub fn record_intervention(&mut self, turn_id: &str) -> bool { + if self.last_intervention_turn_id.as_deref() == Some(turn_id) { + return false; + } + self.last_intervention_turn_id = Some(turn_id.to_string()); + true + } + + pub fn mark_pending_compaction(&mut self, turn_id: &str) { + self.pending_compaction_turn_id = Some(turn_id.to_string()); + } + + pub fn take_pending_compaction(&mut self, turn_id: &str) -> bool { + if self.pending_compaction_turn_id.as_deref() != Some(turn_id) { + return false; + } + self.pending_compaction_turn_id = None; + true + } + + pub fn clear_pending_compaction(&mut self) { + self.pending_compaction_turn_id = None; + } +} + +#[cfg(test)] +mod tests { + use super::EpiphanyCheckpointInterventionState; + + #[test] + fn pending_compaction_is_turn_scoped() { + let mut state = EpiphanyCheckpointInterventionState::default(); + + assert!(state.record_intervention("turn-a")); + assert!(!state.record_intervention("turn-a")); + assert!(!state.take_pending_compaction("turn-a")); + + state.mark_pending_compaction("turn-a"); + + assert!(!state.take_pending_compaction("turn-b")); + assert!(state.take_pending_compaction("turn-a")); + assert!(!state.take_pending_compaction("turn-a")); + } + + #[test] + fn pending_compaction_can_be_cleared() { + let mut state = EpiphanyCheckpointInterventionState::default(); + + state.mark_pending_compaction("turn-a"); + state.clear_pending_compaction(); + + assert!(!state.take_pending_compaction("turn-a")); + } +} diff --git a/epiphany-codex-bridge/src/lib.rs b/epiphany-codex-bridge/src/lib.rs index 35ca6ebc..c8ecd4ae 100644 --- a/epiphany-codex-bridge/src/lib.rs +++ b/epiphany-codex-bridge/src/lib.rs @@ -1,3 +1,4 @@ +pub mod checkpoint; pub mod context; pub mod coordinator; pub mod invalidation; diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 37ba5378..b5a38d2a 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -797,6 +797,12 @@ request. `epiphany_automation.rs` still gathers live host facts and executes the host side effects: submit `Op::Compact`, call `epiphany_launch_job`, and emit JSON-RPC state-update notifications. +The pre-compaction checkpoint intervention latch has also left app-server +ownership. `epiphany-codex-bridge::checkpoint::EpiphanyCheckpointInterventionState` +owns the one-steer-per-turn and same-turn pending-compaction invariant; vendored +`ThreadState` only stores and delegates the latch while event handling remains +host lifecycle glue. + The native OpenAI auth spine overcut has been corrected. It no longer owns a clone of Codex keyring/file/env auth or ChatGPT token refresh; it re-exports vendored `codex-login` and carries the Codex workspace tungstenite patches diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 4e1ad92e..a8496f75 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index 8d4a8b5c..7b89d44a 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: > - The OpenAI auth spine overcut was corrected: `epiphany-openai-auth-spine` - deleted its native clone of Codex env/file/keyring auth and ChatGPT token - refresh, now re-exports retained vendored `codex-login`, and carries the - Codex workspace tungstenite patches needed for standalone builds. This - preserves honest Codex-compatible subscription auth while keeping typed - Epiphany request/status/event/runtime surfaces outside the Codex host brain. - next_action: Continue the Codex starvation campaign at the next Epiphany-in-vendor evacuation surface; do not cut the retained `codex-login` auth organ unless replacing it with an explicitly permitted OpenAI path rather than a lookalike. + The pre-compaction checkpoint intervention latch moved out of vendored + app-server `ThreadState` fields/tests into + `epiphany-codex-bridge::checkpoint::EpiphanyCheckpointInterventionState`. + Codex still holds the per-thread latch as host state, but the Epiphany + invariant is no longer defined and tested inside the app-server organ. + 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/app-server/src/thread_state.rs b/vendor/codex/codex-rs/app-server/src/thread_state.rs index 5e1a64e1..fddd1c92 100644 --- a/vendor/codex/codex-rs/app-server/src/thread_state.rs +++ b/vendor/codex/codex-rs/app-server/src/thread_state.rs @@ -9,6 +9,7 @@ use codex_core::ThreadConfigSnapshot; use codex_protocol::ThreadId; use codex_protocol::protocol::EventMsg; use codex_utils_absolute_path::AbsolutePathBuf; +use epiphany_codex_bridge::checkpoint::EpiphanyCheckpointInterventionState; use std::collections::HashMap; use std::collections::HashSet; use std::path::PathBuf; @@ -65,8 +66,7 @@ pub(crate) struct ThreadState { pub(crate) listener_generation: u64, listener_command_tx: Option>, current_turn_history: ThreadHistoryBuilder, - last_epiphany_checkpoint_intervention_turn_id: Option, - pending_epiphany_checkpoint_compaction_turn_id: Option, + epiphany_checkpoint_intervention: EpiphanyCheckpointInterventionState, listener_thread: Option>, } @@ -99,7 +99,8 @@ impl ThreadState { } self.listener_command_tx = None; self.current_turn_history.reset(); - self.pending_epiphany_checkpoint_compaction_turn_id = None; + self.epiphany_checkpoint_intervention + .clear_pending_compaction(); self.listener_thread = None; } @@ -130,71 +131,29 @@ impl ThreadState { } pub(crate) fn record_epiphany_checkpoint_intervention(&mut self, turn_id: &str) -> bool { - if self - .last_epiphany_checkpoint_intervention_turn_id - .as_deref() - == Some(turn_id) - { - return false; - } - self.last_epiphany_checkpoint_intervention_turn_id = Some(turn_id.to_string()); - true + self.epiphany_checkpoint_intervention + .record_intervention(turn_id) } pub(crate) fn mark_epiphany_checkpoint_intervention_pending_compaction( &mut self, turn_id: &str, ) { - self.pending_epiphany_checkpoint_compaction_turn_id = Some(turn_id.to_string()); + self.epiphany_checkpoint_intervention + .mark_pending_compaction(turn_id); } pub(crate) fn take_epiphany_checkpoint_intervention_pending_compaction( &mut self, turn_id: &str, ) -> bool { - if self - .pending_epiphany_checkpoint_compaction_turn_id - .as_deref() - != Some(turn_id) - { - return false; - } - self.pending_epiphany_checkpoint_compaction_turn_id = None; - true + self.epiphany_checkpoint_intervention + .take_pending_compaction(turn_id) } pub(crate) fn clear_epiphany_checkpoint_intervention_pending_compaction(&mut self) { - self.pending_epiphany_checkpoint_compaction_turn_id = None; - } -} - -#[cfg(test)] -mod tests { - use super::ThreadState; - - #[test] - fn epiphany_checkpoint_intervention_pending_compaction_is_turn_scoped() { - let mut state = ThreadState::default(); - - assert!(state.record_epiphany_checkpoint_intervention("turn-a")); - assert!(!state.record_epiphany_checkpoint_intervention("turn-a")); - assert!(!state.take_epiphany_checkpoint_intervention_pending_compaction("turn-a")); - - state.mark_epiphany_checkpoint_intervention_pending_compaction("turn-a"); - - assert!(!state.take_epiphany_checkpoint_intervention_pending_compaction("turn-b")); - assert!(state.take_epiphany_checkpoint_intervention_pending_compaction("turn-a")); - assert!(!state.take_epiphany_checkpoint_intervention_pending_compaction("turn-a")); - } - - #[test] - fn epiphany_checkpoint_intervention_pending_compaction_can_be_cleared() { - let mut state = ThreadState::default(); - - state.mark_epiphany_checkpoint_intervention_pending_compaction("turn-a"); - state.clear_epiphany_checkpoint_intervention_pending_compaction(); - - assert!(!state.take_epiphany_checkpoint_intervention_pending_compaction("turn-a")); + self.epiphany_checkpoint_intervention + .clear_pending_compaction(); } }