Read Epiphany heartbeat results from runtime spine

This commit is contained in:
Meta Vens 2026-05-06 11:38:10 +01:00
commit 7e962ed796
10 changed files with 563 additions and 208 deletions

View file

@ -74,6 +74,7 @@ pub use runtime_spine::EpiphanyRuntimeEvent;
pub use runtime_spine::EpiphanyRuntimeIdentity;
pub use runtime_spine::EpiphanyRuntimeJob;
pub use runtime_spine::EpiphanyRuntimeJobResult;
pub use runtime_spine::EpiphanyRuntimeJobSnapshot;
pub use runtime_spine::EpiphanyRuntimeJobStatus;
pub use runtime_spine::EpiphanyRuntimeSession;
pub use runtime_spine::EpiphanyRuntimeSessionStatus;
@ -94,8 +95,10 @@ pub use runtime_spine::append_runtime_event;
pub use runtime_spine::complete_runtime_job;
pub use runtime_spine::create_runtime_job;
pub use runtime_spine::create_runtime_session;
pub use runtime_spine::ensure_runtime_session;
pub use runtime_spine::initialize_runtime_spine;
pub use runtime_spine::runtime_hello_frame;
pub use runtime_spine::runtime_job_snapshot;
pub use runtime_spine::runtime_spine_cache;
pub use runtime_spine::runtime_spine_status;
pub use runtime_spine::write_runtime_hello_frame;

View file

@ -231,6 +231,12 @@ pub struct RuntimeSpineJobResultOptions {
pub artifact_refs: Vec<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct EpiphanyRuntimeJobSnapshot {
pub job: EpiphanyRuntimeJob,
pub result: Option<EpiphanyRuntimeJobResult>,
}
pub fn runtime_spine_cache(store_path: impl AsRef<Path>) -> Result<CultCache> {
let store_path = store_path.as_ref();
let mut cache = CultCache::new();
@ -307,6 +313,42 @@ pub fn create_runtime_session(
Ok(session)
}
pub fn ensure_runtime_session(
store_path: impl AsRef<Path>,
options: RuntimeSpineSessionOptions,
) -> Result<EpiphanyRuntimeSession> {
validate_non_empty(&options.session_id, "session id")?;
validate_non_empty(&options.objective, "objective")?;
validate_non_empty(&options.created_at, "created at")?;
let mut cache = runtime_spine_cache(store_path.as_ref())?;
cache.pull_all_backing_stores()?;
require_identity(&cache)?;
if let Some(existing) = cache.get::<EpiphanyRuntimeSession>(&options.session_id)? {
if matches!(
existing.status,
EpiphanyRuntimeSessionStatus::Completed | EpiphanyRuntimeSessionStatus::Archived
) {
return Err(anyhow!(
"runtime session {:?} is terminal and cannot accept jobs",
options.session_id
));
}
return Ok(existing);
}
let session = EpiphanyRuntimeSession {
schema_version: RUNTIME_SPINE_SCHEMA_VERSION.to_string(),
session_id: options.session_id.clone(),
objective: options.objective,
status: EpiphanyRuntimeSessionStatus::Active,
created_at: options.created_at.clone(),
updated_at: options.created_at,
coordinator_note: options.coordinator_note,
metadata: BTreeMap::new(),
};
cache.put(&options.session_id, &session)?;
Ok(session)
}
pub fn create_runtime_job(
store_path: impl AsRef<Path>,
options: RuntimeSpineJobOptions,
@ -361,6 +403,32 @@ pub fn create_runtime_job(
Ok(job)
}
pub fn runtime_job_snapshot(
store_path: impl AsRef<Path>,
job_id: &str,
) -> Result<Option<EpiphanyRuntimeJobSnapshot>> {
validate_non_empty(job_id, "job id")?;
let store_path = store_path.as_ref();
if !store_path.exists() {
return Ok(None);
}
let mut cache = runtime_spine_cache(store_path)?;
cache.pull_all_backing_stores()?;
let Some(job) = cache.get::<EpiphanyRuntimeJob>(job_id)? else {
return Ok(None);
};
let result = cache
.get_all::<EpiphanyRuntimeJobResult>()?
.into_iter()
.filter(|result| result.job_id == job_id)
.max_by(|left, right| {
left.completed_at
.cmp(&right.completed_at)
.then_with(|| left.result_id.cmp(&right.result_id))
});
Ok(Some(EpiphanyRuntimeJobSnapshot { job, result }))
}
pub fn complete_runtime_job(
store_path: impl AsRef<Path>,
options: RuntimeSpineJobResultOptions,
@ -720,6 +788,16 @@ mod tests {
assert_eq!(status.open_jobs, 0);
assert_eq!(status.job_results, 1);
assert_eq!(status.events, 2);
let snapshot =
runtime_job_snapshot(&store, "job-1")?.expect("completed job snapshot should exist");
assert_eq!(snapshot.job.status, EpiphanyRuntimeJobStatus::Completed);
assert_eq!(
snapshot
.result
.as_ref()
.map(|result| result.result_id.as_str()),
Some("result-1")
);
Ok(())
}

View file

@ -23,7 +23,7 @@ The spine exists in more than twenty-five live paths:
- explicit state updates: app-server routes `thread/epiphany/update` through a loaded `CodexThread` update method that rejects malformed appended observation/evidence graph records and structurally invalid replacement fields, including investigation checkpoints that cite missing evidence and planning records with broken ids/source refs/backlog links/draft acceptance criteria, mutates live `SessionState` only after validation, bumps the revision, and persists an immediate `RolloutItem::EpiphanyState` snapshot in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4620), [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:376), [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:552), [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:1294), and [promotion.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/promotion.rs:44).
- live state notifications and write responses: app-server declares experimental `thread/epiphany/stateUpdated` and emits it with the updated typed state, typed `source`, event-level `revision`, and typed `changedFields` after successful direct updates and accepted promotions, but not after rejected promotions; direct `thread/epiphany/update` and accepted `thread/epiphany/promote` responses expose the same revision/change metadata, accepted promotions always report `Evidence` because verifier evidence is appended even when the patch evidence list is empty, and successful write response/notification states now use the same client-visible live projection as `thread/read`, including retrieval-summary backfill when durable state has no persisted retrieval metadata, in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:1113), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4354), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4419), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4428), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4555), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4608), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4652), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4693), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11327), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11370).
- scene projection: app-server declares experimental read-only `thread/epiphany/scene`, reads the same live/stored Epiphany state view used by `thread/read`, and derives a compact client scene with objective, active subgoal, invariant status counts, graph focus/counts, retrieval status, investigation-checkpoint summary counts, observation/evidence summaries, churn, live/stored source, and available control-plane actions without mutating or persisting state in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:340), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:3924), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4115), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10757).
- job/progress reflection: app-server declares experimental read-only `thread/epiphany/jobs`, reads the same thread view as scene, optionally fills retrieval state for loaded threads with no Epiphany state, and derives four non-authoritative progress slots for retrieval indexing, graph remap, invariant verification, and specialist work. Durable `job_bindings` now act as a thin Epiphany-owned launcher seam with launcher id, authority scope, backend kind, and backend job id. New bindings use the `heartbeat` backend and project as pending heartbeat turns until typed heartbeat/runtime-spine result reporting lands; old `agent_jobs` bindings remain a legacy read overlay only.
- job/progress reflection: app-server declares experimental read-only `thread/epiphany/jobs`, reads the same thread view as scene, optionally fills retrieval state for loaded threads with no Epiphany state, and derives four non-authoritative progress slots for retrieval indexing, graph remap, invariant verification, and specialist work. Durable `job_bindings` now act as a thin Epiphany-owned launcher seam with launcher id, authority scope, backend kind, and backend job id. New bindings use the `heartbeat` backend and app-server opens matching typed runtime-spine job receipts under `state/runtime-spine.msgpack`; old `agent_jobs` bindings remain a legacy read overlay only.
- bound-job progress notifications: app-server still declares experimental `thread/epiphany/jobsUpdated` for legacy Codex evacuation telemetry. It can translate old `agent_job_progress:{json}` background events for old bindings, but heartbeat-owned activation is now the live Epiphany path.
- explicit job control: app-server declares experimental `thread/epiphany/jobLaunch` and `thread/epiphany/jobInterrupt`, routes them through core launch/interrupt handlers that persist heartbeat-backed durable `job_bindings` revisions through the authoritative Epiphany state path, and emit `thread/epiphany/stateUpdated` with source `jobLaunch` or `jobInterrupt` instead of creating SQLite `agent_jobs` rows or pretending jobs reflection is the scheduler.
- freshness reflection: app-server declares experimental read-only `thread/epiphany/freshness`, reads the same live/stored thread view used by scene/jobs/context, fills live retrieval state for loaded threads, and derives one bounded retrieval/graph freshness lens plus, for loaded threads, watcher-backed invalidation inputs with watched root, changed paths, mapped graph-node hits, active-frontier hits, and revision/source identity without mutating, scheduling, notifying, or performing automatic semantic invalidation in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:350), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4167), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4259), [epiphany_invalidation.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/epiphany_invalidation.rs:19), [epiphany_invalidation.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/epiphany_invalidation.rs:67), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4219), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11073).
@ -35,15 +35,15 @@ The spine exists in more than twenty-five live paths:
- reorientation policy: app-server declares experimental read-only `thread/epiphany/reorient`, reuses the same live/stored thread view plus mapped pressure/freshness/watcher signals, and derives a bounded `resume`/`regather` verdict with checkpoint-path/frontier reasons without mutating, compacting, scheduling, notifying, or continuing work in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:365), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4514), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4394), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11424).
- CRRC coordinator recommendation: app-server declares experimental read-only `thread/epiphany/crrc`, reuses the same pressure/freshness/watcher/reorientation mapping plus the fixed `reorient-worker` binding and result read-back helper, recognizes already accepted reorientation findings so an accepted result is not recommended for acceptance again, and returns one bounded recommendation such as continue, prepare checkpoint, launch worker, wait, review, accept, or regather manually without mutating, launching, accepting, compacting, notifying, scheduling, or silently continuing work in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:370), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4661), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4546).
- role ownership projection: app-server declares experimental read-only `thread/epiphany/roles`, reuses the same state/jobs/pressure/reorient/result/CRRC signals as the coordinator loop, and projects implementation, Imagination/planning, modeling/checkpoint, verification/review, and reorientation lanes with owner roles, status, linked jobs, authority scopes, and recommended scene actions without launching, scheduling, accepting, compacting, notifying, or mutating state in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:350), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4161), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4310).
- role specialist launch/read-back/acceptance: app-server declares experimental `thread/epiphany/roleLaunch`, read-only `thread/epiphany/roleResult`, and write-gated `thread/epiphany/roleAccept`. `roleLaunch` accepts fixed Imagination/planning, modeling/checkpoint, and verification/review role ids, builds bounded specialist payloads from authoritative Epiphany state, routes them through the existing `epiphany_launch_job` job-control seam, and emits the normal `jobLaunch` state update for `jobBindings` only. `roleResult` resolves the fixed or requested binding through the current `agent_jobs` backend and projects structured output as a reviewable finding with verdict, summary, next safe move, inspected files, frontier/evidence ids, optional `statePatch`, optional `selfPatch`, coordinator `selfPersistence` review, raw result, and errors without mutation, promotion, scheduling, or hidden continuation. `selfPatch` is a Ghostlight-shaped lane-memory request, not project truth: the coordinator accepts only role-matched, bounded, evidence-grounded memory/goal/value/private-note mutations and refuses objectives, graphs, checkpoints, scratch, planning, jobs, authority, code edits, or raw transcripts with reasons. `roleAccept` accepts completed Imagination findings only when they contain a valid planning-only patch with draft Objective Draft review gates, accepts completed modeling/checkpoint findings only with a modeling-safe `statePatch`, applies accepted project patches through the existing Epiphany state validator, appends audit observation/evidence, and carries the same `selfPersistence` review. GUI/coordinator accept paths then apply accepted `selfPatch` requests to typed CultCache role dossiers in `state/agents.msgpack` through the native `epiphany-agent-memory-store` binary. Implementation stays owned by the main coding agent, verification output remains review-only, and reorientation stays on the CRRC-specific `reorientLaunch` / `reorientResult` / `reorientAccept` path.
- role specialist launch/read-back/acceptance: app-server declares experimental `thread/epiphany/roleLaunch`, read-only `thread/epiphany/roleResult`, and write-gated `thread/epiphany/roleAccept`. `roleLaunch` accepts fixed Imagination/planning, modeling/checkpoint, and verification/review role ids, builds bounded specialist payloads from authoritative Epiphany state, routes them through the heartbeat-backed `epiphany_launch_job` seam, opens a typed runtime-spine job receipt, and emits the normal `jobLaunch` state update for `jobBindings` only. `roleResult` resolves heartbeat bindings through typed runtime-spine job results first, falling back to legacy `agent_jobs` only for old bindings, and projects structured output as a reviewable finding with verdict, summary, next safe move, inspected files, frontier/evidence ids, optional `statePatch`, optional `selfPatch`, coordinator `selfPersistence` review, raw result, and errors without mutation, promotion, scheduling, or hidden continuation. `selfPatch` is a Ghostlight-shaped lane-memory request, not project truth: the coordinator accepts only role-matched, bounded, evidence-grounded memory/goal/value/private-note mutations and refuses objectives, graphs, checkpoints, scratch, planning, jobs, authority, code edits, or raw transcripts with reasons. `roleAccept` accepts completed heartbeat findings from typed runtime-spine results, accepts completed Imagination findings only when they contain a valid planning-only patch with draft Objective Draft review gates, accepts completed modeling/checkpoint findings only with a modeling-safe `statePatch`, applies accepted project patches through the existing Epiphany state validator, appends audit observation/evidence, and carries the same `selfPersistence` review. GUI/coordinator accept paths then apply accepted `selfPatch` requests to typed CultCache role dossiers in `state/agents.msgpack` through the native `epiphany-agent-memory-store` binary. Implementation stays owned by the main coding agent, verification output remains review-only, and reorientation stays on the CRRC-specific `reorientLaunch` / `reorientResult` / `reorientAccept` path.
- shared prompt memory projection and role dossiers: the prompt config now carries `[shared].persistent_memory` in [epiphany_specialists.toml](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/prompts/epiphany_specialists.toml:2). App-server parses that block, prepends it through `epiphany_agent_prompt_with_memory`, and therefore projects the same Ghostlight-derived memory into fixed role specialist instructions, reorientation-worker instructions, coordinator notes, and CRRC checkpoint steering in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:13260), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:13349), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:13594), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:13603), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:16119). The GUI-launched Hands implementation lane prepends the same memory in [epiphany_gui_action.py](E:/Projects/EpiphanyAgent/tools/epiphany_gui_action.py:52), while the base `<epiphany_state>` doctrine reinforces the Perfect Machine rule through [prompt.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/prompt.rs:31) and [epiphany_doctrine.md](E:/Projects/EpiphanyAgent/epiphany-core/src/prompts/epiphany_doctrine.md:1). Individual Ghostlight-shaped role dossiers now live in typed CultCache `state/agents.msgpack`, and the native `epiphany-agent-memory-store` binary validates, reviews, and applies bounded self-memory patches through the Rust store.
- fixed-lane coordinator: app-server declares experimental read-only `thread/epiphany/coordinator`, composes role, pressure, reorient, CRRC, role result, and reorient result signals, and returns one current action without mutation. Review acceptance and implementation clearance are intentionally different: an accepted verification finding clears implementation only when the verifier verdict is `pass`; accepted `needs-evidence`, `needs-review`, or `fail` findings route back toward modeling/checkpoint strengthening before code work resumes. `regatherManually` is now fallback, not a supervisor-puppeteering excuse: completed unaccepted modeling findings stop at `reviewModelingResult`, modeling acceptance precedes verification, and fixed lane actions are considered before manual regather.
- MVP operator view: native `epiphany-mvp-status` launches app-server over stdio, starts or reads one thread, gathers scene, planning, pressure, reorient, jobs, roles, Imagination/modeling/verification role results, reorient result, CRRC responses, heartbeat, and Face bubble artifacts, and renders them as text or JSON so the loop can be dogfooded without Python owning the product surface.
- MVP dogfood runner: `tools/epiphany_mvp_dogfood.py` launches app-server over stdio, drives a bounded state/role/CRRC/reorientation loop, and writes operator-safe JSON/text artifacts plus sealed transcript diagnostics under `.epiphany-dogfood/mvp-loop` so a human can audit the run through projections and compare it against a vanilla Codex reference.
- Agent function/API telemetry: native `epiphany-agent-telemetry` mechanically parses sealed AppServerClient JSONL transcripts into operator-safe telemetry with method counts, request shape, job/status/path fields, and visible function/tool names while sealing text, turn items, raw results, and direct agent messages.
- reorientation launch: app-server now declares experimental `thread/epiphany/reorientLaunch`, requires a loaded thread plus durable investigation checkpoint, consumes the live reorientation verdict, and launches one fixed `reorient-worker` binding through the existing job-control seam with explicit resume-versus-regather scope, checkpoint-derived payload, and structured-output expectations instead of pretending CRRC suddenly grew a scheduler in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:370), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4575), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4519).
- reorientation result read-back: app-server now declares experimental read-only `thread/epiphany/reorientResult`, defaults to the fixed `reorient-worker` binding, resolves that binding through the current `agent_jobs` backend when available, reads the single backend item result, and projects a reviewable finding with mode, summary, next safe move, checkpoint-validity flag, inspected files, frontier ids, evidence ids, raw result, and any job/item errors without mutating state, promoting evidence, or scheduling follow-up work in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:374), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4610), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4684).
- reorientation finding acceptance: app-server now declares experimental `thread/epiphany/reorientAccept`, requires a loaded thread and a completed `reorient-worker` backend result, appends accepted observation/evidence records, optionally banks the finding into scratch and the durable investigation checkpoint, persists through the normal typed update path, and emits `thread/epiphany/stateUpdated` with source `reorientAccept` without accepting pending work, auto-promoting arbitrary worker output, launching follow-up work, or silently continuing implementation in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:378), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4635), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4885).
- reorientation result read-back: app-server now declares experimental read-only `thread/epiphany/reorientResult`, defaults to the fixed `reorient-worker` binding, resolves heartbeat bindings through typed runtime-spine job results first, falls back to legacy `agent_jobs` only for old bindings, and projects a reviewable finding with mode, summary, next safe move, checkpoint-validity flag, inspected files, frontier ids, evidence ids, raw result, and any errors without mutating state, promoting evidence, or scheduling follow-up work.
- reorientation finding acceptance: app-server now declares experimental `thread/epiphany/reorientAccept`, requires a loaded thread and a completed `reorient-worker` result, accepts heartbeat-backed results from typed runtime-spine receipts before falling back to old bindings, appends accepted observation/evidence records, optionally banks the finding into scratch and the durable investigation checkpoint, persists through the normal typed update path, and emits `thread/epiphany/stateUpdated` with source `reorientAccept` without accepting pending work, auto-promoting arbitrary worker output, launching follow-up work, or silently continuing implementation in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:378), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4635), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4885).
- retrieval/indexing: app-server routes `thread/epiphany/retrieve` and `thread/epiphany/index` through loaded `CodexThread` host methods into `epiphany-core` in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4356), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4432), [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:467), and [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:449).
The thick Epiphany-owned implementation is mostly outside vendored Codex now:

View file

@ -31,10 +31,10 @@ remembers doctrine; the branch remembers the blade.
- Do not copy exact branch or HEAD from this note. Run `git status --short --branch` and `git log --oneline -5`.
- Phase 1 through Phase 5 are complete enough.
- Phase 6 has read-only `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/roles`, `thread/epiphany/freshness`, `thread/epiphany/context`, `thread/epiphany/graphQuery`, `thread/epiphany/planning`, `thread/epiphany/pressure`, `thread/epiphany/reorient`, `thread/epiphany/crrc`, `thread/epiphany/coordinator`, `thread/epiphany/reorientResult`, and `thread/epiphany/roleResult`; durable `jobBindings` now act as a thin Epiphany-owned launcher seam with launcher id, authority scope, and backend kind/job id. New `thread/epiphany/jobLaunch`, `thread/epiphany/jobInterrupt`, `thread/epiphany/roleLaunch`, and `thread/epiphany/reorientLaunch` writes are heartbeat-backed and no longer create Codex `agent_jobs` rows or require the SQLite state runtime. Freshness carries watcher-backed invalidation inputs, graphQuery traverses authoritative typed graph neighborhoods and path/symbol matches without mutation, planning projects typed captures/backlog/roadmap/objective drafts without adopting work, roles project implementation/imagination/modeling/verification/reorientation ownership from existing signals without becoming a scheduler, `roleResult` and `reorientResult` report heartbeat-backed bindings as pending until typed heartbeat/runtime-spine results exist, `roleAccept` and `reorientAccept` remain explicit review gates, `thread/epiphany/crrc` recommends the next explicit CRRC action without launching, accepting, compacting, scheduling, or mutating, and `thread/epiphany/coordinator` composes those signals into a fixed-lane MVP action recommendation without becoming a writer.
- Phase 6 has read-only `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/roles`, `thread/epiphany/freshness`, `thread/epiphany/context`, `thread/epiphany/graphQuery`, `thread/epiphany/planning`, `thread/epiphany/pressure`, `thread/epiphany/reorient`, `thread/epiphany/crrc`, `thread/epiphany/coordinator`, `thread/epiphany/reorientResult`, and `thread/epiphany/roleResult`; durable `jobBindings` now act as a thin Epiphany-owned launcher seam with launcher id, authority scope, and backend kind/job id. New `thread/epiphany/jobLaunch`, `thread/epiphany/jobInterrupt`, `thread/epiphany/roleLaunch`, and `thread/epiphany/reorientLaunch` writes are heartbeat-backed, open typed runtime-spine job receipts under `state/runtime-spine.msgpack`, and no longer create Codex `agent_jobs` rows or require the SQLite state runtime. Freshness carries watcher-backed invalidation inputs, graphQuery traverses authoritative typed graph neighborhoods and path/symbol matches without mutation, planning projects typed captures/backlog/roadmap/objective drafts without adopting work, roles project implementation/imagination/modeling/verification/reorientation ownership from existing signals without becoming a scheduler, `roleResult` and `reorientResult` read heartbeat-backed typed runtime-spine job results when present, `roleAccept` and `reorientAccept` accept completed heartbeat findings from typed runtime-spine results while remaining explicit review gates, `thread/epiphany/crrc` recommends the next explicit CRRC action without launching, accepting, compacting, scheduling, or mutating, and `thread/epiphany/coordinator` composes those signals into a fixed-lane MVP action recommendation without becoming a writer.
- Native `epiphany-mvp-status` is the first dogfood operator view. It starts or reads a thread through app-server and prints scene, planning, pressure, reorient, jobs, roles, Imagination/modeling/verification role result read-backs, reorient result, heartbeat, Face bubbles, and CRRC recommendation as text or JSON. The Python status module is now quarantine tissue for still-Python GUI/status callers, not the smoked product path.
- Native `epiphany-mvp-coordinator` is the first auditable fixed-lane coordinator runner. It starts or reads a thread through app-server, opens a native runtime-spine session, follows the harness-native coordinator action, can auto-launch modeling, verification, or reorient-worker jobs, records native runtime job/result receipts for terminal launched work, keeps semantic findings review-gated by default, and writes summary, steps, rendered snapshots, transcript, stderr, runtime-spine status, and final next-action artifacts under `.epiphany-dogfood/coordinator` or a caller-provided artifact directory. It refuses direct backend-completion mutation; full completion smoke needs live workers while execution is being cauterized into CultNet.
- Native `epiphany-runtime-spine` is the first Codex-independent runtime vertebra. It owns typed CultCache documents for runtime identity, sessions, jobs, job results, and events; opens/completes native jobs; projects job-result counts; and can emit a framed CultNet hello message advertising the native document contract. Codex app-server launch/read-back is now a heartbeat binding bridge; the old `agent_jobs` SQLite path is legacy read-back only for stale bindings.
- Native `epiphany-runtime-spine` is the first Codex-independent runtime vertebra. It owns typed CultCache documents for runtime identity, sessions, jobs, job results, and events; opens/completes native jobs; snapshots jobs/results by runtime job id; projects job-result counts; and can emit a framed CultNet hello message advertising the native document contract. Codex app-server launch/read-back/acceptance is now a typed heartbeat/runtime-spine bridge; the old `agent_jobs` SQLite path is legacy read-back only for stale bindings.
- Native CRRC automation is now landed only at turn-complete safe boundaries. It may submit `Op::Compact` for coordinator-approved `compactRehydrateReorient` or for a successful pre-compaction checkpoint intervention's pending compact handoff, and it may launch the fixed `reorient-worker` for coordinator-approved `launchReorientWorker`. It does not auto-launch Imagination/modeling/verification, accept findings, promote evidence, edit implementation code, or keep going after reviewable semantic output.
- Pre-compaction checkpoint intervention is now landed. On token-count events for loaded Epiphany threads, when current context usage reaches 80% of the active auto-compact/context limit, the harness steers the active turn once with a CRRC checkpoint directive so the agent banks working context before compaction/reorientation. Pressure ignores cumulative token spend; cumulative-only telemetry reports unknown instead of yelling. A successful steer now latches a turn-scoped compact handoff that is consumed at clean turn completion, preventing the brake from decaying into another implementation turn. This is still bounded steering plus compaction handoff, not automatic semantic acceptance, a broad scheduler, or implementation continuation.
- `tools/epiphany_mvp_dogfood.py` is the first auditable dogfood runner. It drives a bounded state/role/CRRC/reorientation loop and writes local artifacts under `.epiphany-dogfood/mvp-loop`, including sealed app-server transcript diagnostics, rendered snapshots, operator-safe final status, vanilla-reference prompt/output, and comparison notes. The runner now writes truthful vanilla/comparison artifacts whether the optional vanilla reference is skipped, fails, or completes.

Binary file not shown.

View file

@ -94,7 +94,7 @@ accepted_design:
- GUI/client work must reflect or steer typed state rather than manufacture a second throne of truth.
- The Aquarium operator UI now lives in sibling repo `E:\Projects\EpiphanyAquarium`, with its own `AGENTS.md`, `state/map.yaml`, `state/memory.json`, and interface doctrine. It remains a local Tauri v2 + React/WebGL client over EpiphanyAgent's app-server/tooling surfaces; app-server and typed Epiphany state remain authoritative.
- EpiphanyAgent no longer carries the embedded `apps/epiphany-gui` source tree. Backend artifact directories and action tools still use the historical `.epiphany-gui` contract until that backend naming seam is deliberately renamed.
- Durable `job_bindings` now form a thin Epiphany-owned launcher seam: new specialist/reorient launches write heartbeat-backed bindings with launcher identity, authority scope, backend kind, and backend job id. The older runtime-agent-job pointer remains schema compatibility only; new Epiphany launches do not create Codex `agent_jobs` rows.
- Durable `job_bindings` now form a thin Epiphany-owned launcher seam: new specialist/reorient launches write heartbeat-backed bindings with launcher identity, authority scope, backend kind, and backend job id, and open matching typed runtime-spine job receipts under `state/runtime-spine.msgpack`. The older runtime-agent-job pointer remains schema compatibility only; new Epiphany launches do not create Codex `agent_jobs` rows.
- `thread/epiphany/jobs` is a read-only job/progress reflection: it derives indexing, remap, verification, and specialist slots from authoritative state and retrieval summaries, projects heartbeat-backed bindings as pending heartbeat turns, and only treats `agent_jobs` as a legacy read overlay for old bindings. It is not the scheduler, notification stream, or durable runtime job table.
- `thread/epiphany/jobsUpdated` is legacy Codex evacuation telemetry only: it can still translate old runtime `agent_job_progress:{json}` background events for old bindings, but heartbeat-owned Epiphany activation is the live path.
- `thread/epiphany/freshness` is the first read-only freshness surface: it derives retrieval and graph staleness from live retrieval summaries plus frontier/churn state, and for loaded threads reflects watcher-backed invalidation inputs with changed paths plus mapped graph/frontier hits, without mutating, scheduling, or performing automatic semantic invalidation.
@ -108,19 +108,18 @@ accepted_design:
- Coordinator implementation clearance is stricter than review acceptance: an accepted verification/review finding only opens the implementation gate when its verdict is `pass`; accepted `needs-evidence`, `needs-review`, or `fail` findings send the loop back toward modeling/checkpoint strengthening or reorientation instead of blessing a sick machine.
- Coordinator regather handling is now fixed-lane first, not supervisor improvisation: `regatherManually` is a fallback only after reorient/modeling/verification review and launch lanes cannot advance the state, completed modeling findings must be reviewed before verification, and the CLI coordinator's explicit test-only `--auto-review` mode may accept modeling state patches before launching verification while production semantics remain review-gated.
- Native `epiphany-mvp-coordinator` is the first auditable coordinator runner: it drives the fixed single-user lanes through app-server APIs, opens a native runtime-spine session, records native runtime job/result receipts for launched specialists and reorient workers when terminal results arrive, keeps semantic findings review-gated by default, and writes coordinator summaries, JSONL steps, rendered snapshots, transcript, stderr, runtime-spine status, and final next-action artifacts. It refuses direct backend-completion mutation; full specialist completion smoke must use live workers while the execution path is cauterized into CultNet.
- Native `epiphany-runtime-spine` is the first Codex-independent runtime vertebra: it initializes and inspects typed CultCache `epiphany.runtime.identity`, `epiphany.runtime.session`, `epiphany.runtime.job`, `epiphany.runtime.job_result`, and `epiphany.runtime.event` documents, opens/completes native jobs with result records, and can emit a framed CultNet hello message advertising the native document contract. This is the new source-of-truth target for runtime identity/session/job/result/event state; Codex app-server is only a temporary evacuation bridge until those flows are migrated.
- Native `epiphany-runtime-spine` is the first Codex-independent runtime vertebra: it initializes and inspects typed CultCache `epiphany.runtime.identity`, `epiphany.runtime.session`, `epiphany.runtime.job`, `epiphany.runtime.job_result`, and `epiphany.runtime.event` documents, opens/completes native jobs with result records, snapshots jobs by id, and can emit a framed CultNet hello message advertising the native document contract. App-server launch now opens typed runtime jobs, `roleResult` / `reorientResult` read heartbeat-backed typed job results before falling back to legacy `agent_jobs` rows, and `roleAccept` / `reorientAccept` accept completed heartbeat findings from the typed runtime-spine path without requiring SQLite.
- Limited native CRRC automation is now wired only at turn-complete safe boundaries: it can submit a compact operation when the read-only coordinator recommends `compactRehydrateReorient` or when a successful pre-compaction checkpoint intervention left a pending compact handoff for the completed turn, and it can launch the fixed `reorient-worker` when the coordinator recommends `launchReorientWorker`. It does not auto-launch modeling or verification, auto-accept findings, promote evidence, edit implementation code, or continue after reviewable semantic results; the rite may carry the stretcher, not declare the patient healed.
- 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 JSON without becoming a state store. The Python status module remains quarantine tissue for still-Python GUI/status callers until those organs are cut over.
- `thread/epiphany/roles` is the first 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 can be launched through fixed heartbeat-backed bindings and read back as reviewable findings once the heartbeat/runtime spine reports typed 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 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`, appends audit observation/evidence, and rejects verification, implementation, reorientation, job binding, objective adoption, or broad authority fields.
- `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`, appends audit observation/evidence, and rejects verification, implementation, reorientation, job binding, objective adoption, or broad authority fields.
- `thread/epiphany/reorientLaunch` is the first bounded runtime consumer over the reorientation verdict: it launches one fixed `reorient-worker` heartbeat binding with explicit resume-versus-regather scope, authority, and checkpoint-derived payload, without making `thread/epiphany/reorient` a writer or turning CRRC into an ambient scheduler.
- `thread/epiphany/reorientResult` is the first bounded read-back surface for that worker: heartbeat-backed bindings currently report pending until the heartbeat/runtime spine produces typed results; old `agent_jobs` result read-back remains only for legacy bindings.
- `thread/epiphany/reorientAccept` is the first explicit acceptance write for completed reorient-worker findings: it appends accepted observation/evidence records, can optionally update scratch and investigation checkpoint state, and emits `stateUpdated` with source `reorientAccept` without accepting pending work or launching follow-up work.
- `thread/epiphany/reorientResult` is the first bounded read-back surface for that worker: heartbeat-backed bindings read typed runtime-spine job results when present and otherwise report pending; old `agent_jobs` result read-back remains only for legacy bindings.
- `thread/epiphany/reorientAccept` is the first explicit acceptance write for completed reorient-worker findings: it accepts completed heartbeat-backed findings from typed runtime-spine results, appends accepted observation/evidence records, can optionally update scratch and investigation checkpoint state, and emits `stateUpdated` with source `reorientAccept` without accepting pending work or launching follow-up work.
- After `thread/epiphany/reorientAccept`, CRRC recognizes the accepted reorientation finding and does not recommend accepting the same completed result again.
- `tools/epiphany_mvp_dogfood.py` is the auditable MVP dogfood runner: it drives state, role launch/result, CRRC drift, reorient launch/result/accept, rendered status snapshots, operator-safe status artifacts, and sealed app-server transcript diagnostics into a local artifact bundle, and it now writes truthful comparison artifacts whether the optional vanilla Codex reference is skipped, fails, or completes.
- `tools/epiphany_mvp_live_specialist.py` is the auditable live-specialist MVP runner: it launches a real modeling/checkpoint or verification/review specialist through `thread/epiphany/roleLaunch`, lets the spawned worker report through `report_agent_job_result`, reads the projected finding back through `thread/epiphany/roleResult`, and writes sealed transcript plus operator-safe status artifacts without manually completing backend rows.
- Epiphany safety doctrine is to separate cognition from actuation, keep authority explicit, make interruption sacred, stage capability release, and treat operator misuse as a normal design input rather than a rude hypothetical. The machine must have brakes before it receives larger engines.
- Epiphany is built in the open, but publication mode is not one-size-fits-all: use FOSS where viable, source-available licensing where economics or capability risk demand it, and commercial terms where organizations extract real enterprise value.
- `thread/epiphany/jobLaunch` and `thread/epiphany/jobInterrupt` are the first bounded authority surfaces over the thin job seam: they mutate durable `job_bindings`, queue or clear heartbeat-owned activation, and preserve explicit launcher identity and interruption without turning `thread/epiphany/jobs` into a scheduler.
@ -177,7 +176,7 @@ landed_baseline:
- Phase 6 acceptance: explicit reorient-worker finding acceptance through `thread/epiphany/reorientAccept`.
- Phase 6 dogfood view: first CLI operator status over scene/pressure/reorient/jobs/result/CRRC.
- Phase 6 dogfood runner: auditable scripted MVP loop over state, role results, CRRC, reorientation acceptance, status snapshots, operator-safe summaries, and sealed transcript artifacts.
- Phase 6 live specialist runner: auditable real worker loop over roleLaunch, agent_jobs worker execution, report_agent_job_result, roleResult, status rendering, operator-safe summaries, and sealed transcript artifacts.
- Phase 6 live specialist runner needs rebuild: old `agent_jobs` worker execution / `report_agent_job_result` path is no longer current; the replacement must complete typed runtime-spine job results from heartbeat-owned turns.
- Phase 6 role ownership: read-only `thread/epiphany/roles` over implementation, modeling/checkpoint, verification/review, and reorientation lanes.
- Phase 6 role specialists: explicit `thread/epiphany/roleLaunch` and read-only `thread/epiphany/roleResult` templates for modeling/checkpoint and verification/review.
- Phase 6 modeling role acceptance: explicit `thread/epiphany/roleAccept` for reviewed modeling/checkpoint `statePatch` findings that can grow graphs/frontier/checkpoints through the existing state validator.

1
vendor/codex/codex-rs/Cargo.lock generated vendored
View file

@ -1890,6 +1890,7 @@ dependencies = [
"codex-utils-rustls-provider",
"constant_time_eq 0.3.1",
"core_test_support",
"epiphany-core",
"futures",
"gethostname",
"hmac",

View file

@ -63,6 +63,7 @@ codex-utils-absolute-path = { workspace = true }
codex-utils-json-to-toml = { workspace = true }
codex-utils-rustls-provider = { workspace = true }
chrono = { workspace = true }
epiphany-core = { path = "../../../../epiphany-core" }
clap = { workspace = true, features = ["derive"] }
constant_time_eq = { workspace = true }
futures = { workspace = true }

View file

@ -510,6 +510,10 @@ use codex_thread_store::ThreadStoreError;
use codex_thread_store::UpdateThreadMetadataParams as StoreUpdateThreadMetadataParams;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP;
use epiphany_core::EpiphanyRuntimeJobResult;
use epiphany_core::EpiphanyRuntimeJobSnapshot;
use epiphany_core::EpiphanyRuntimeJobStatus;
use epiphany_core::runtime_job_snapshot;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
@ -4433,11 +4437,15 @@ impl CodexMessageProcessor {
&watcher,
);
let mut state_db_ctx = if let Some(loaded_thread) = loaded_thread.as_ref() {
loaded_thread.epiphany_state_runtime().await
} else {
None
};
let (mut state_db_ctx, runtime_store_path) =
if let Some(loaded_thread) = loaded_thread.as_ref() {
(
loaded_thread.epiphany_state_runtime().await,
Some(loaded_thread.epiphany_runtime_spine_store_path().await),
)
} else {
(None, None)
};
if state_db_ctx.is_none() {
state_db_ctx = open_state_db_for_direct_thread_lookup(&self.config).await;
}
@ -4458,6 +4466,7 @@ impl CodexMessageProcessor {
let (result_status, finding, _) = load_epiphany_reorient_result_snapshot(
thread.epiphany_state.as_ref(),
state_db_ctx.as_ref(),
runtime_store_path.as_deref(),
EPIPHANY_REORIENT_LAUNCH_BINDING_ID,
)
.await;
@ -4579,11 +4588,15 @@ impl CodexMessageProcessor {
&watcher,
);
let mut state_db_ctx = if let Some(loaded_thread) = loaded_thread.as_ref() {
loaded_thread.epiphany_state_runtime().await
} else {
None
};
let (mut state_db_ctx, runtime_store_path) =
if let Some(loaded_thread) = loaded_thread.as_ref() {
(
loaded_thread.epiphany_state_runtime().await,
Some(loaded_thread.epiphany_runtime_spine_store_path().await),
)
} else {
(None, None)
};
if state_db_ctx.is_none() {
state_db_ctx = open_state_db_for_direct_thread_lookup(&self.config).await;
}
@ -4604,6 +4617,7 @@ impl CodexMessageProcessor {
let (reorient_result_status, reorient_finding, _) = load_epiphany_reorient_result_snapshot(
thread.epiphany_state.as_ref(),
state_db_ctx.as_ref(),
runtime_store_path.as_deref(),
EPIPHANY_REORIENT_LAUNCH_BINDING_ID,
)
.await;
@ -4643,6 +4657,7 @@ impl CodexMessageProcessor {
load_epiphany_role_result_snapshot(
state,
state_db_ctx.as_ref(),
runtime_store_path.as_deref(),
ThreadEpiphanyRoleId::Modeling,
EPIPHANY_MODELING_ROLE_BINDING_ID,
)
@ -4668,6 +4683,7 @@ impl CodexMessageProcessor {
load_epiphany_role_result_snapshot(
state,
state_db_ctx.as_ref(),
runtime_store_path.as_deref(),
ThreadEpiphanyRoleId::Verification,
EPIPHANY_VERIFICATION_ROLE_BINDING_ID,
)
@ -4985,11 +5001,15 @@ impl CodexMessageProcessor {
return;
}
let mut state_db_ctx = if let Some(loaded_thread) = loaded_thread.as_ref() {
loaded_thread.epiphany_state_runtime().await
} else {
None
};
let (mut state_db_ctx, runtime_store_path) =
if let Some(loaded_thread) = loaded_thread.as_ref() {
(
loaded_thread.epiphany_state_runtime().await,
Some(loaded_thread.epiphany_runtime_spine_store_path().await),
)
} else {
(None, None)
};
if state_db_ctx.is_none() {
state_db_ctx = open_state_db_for_direct_thread_lookup(&self.config).await;
}
@ -4998,9 +5018,14 @@ impl CodexMessageProcessor {
let job = map_epiphany_jobs(Some(state), None, &job_resolution)
.into_iter()
.find(|job| job.id == binding_id);
let (status, finding, note) =
load_epiphany_role_result_snapshot(state, state_db_ctx.as_ref(), role_id, &binding_id)
.await;
let (status, finding, note) = load_epiphany_role_result_snapshot(
state,
state_db_ctx.as_ref(),
runtime_store_path.as_deref(),
role_id,
&binding_id,
)
.await;
self.outgoing
.send_response(
@ -5676,11 +5701,15 @@ impl CodexMessageProcessor {
&watcher,
);
let mut state_db_ctx = if let Some(loaded_thread) = loaded_thread.as_ref() {
loaded_thread.epiphany_state_runtime().await
} else {
None
};
let (mut state_db_ctx, runtime_store_path) =
if let Some(loaded_thread) = loaded_thread.as_ref() {
(
loaded_thread.epiphany_state_runtime().await,
Some(loaded_thread.epiphany_runtime_spine_store_path().await),
)
} else {
(None, None)
};
if state_db_ctx.is_none() {
state_db_ctx = open_state_db_for_direct_thread_lookup(&self.config).await;
}
@ -5701,6 +5730,7 @@ impl CodexMessageProcessor {
let (result_status, finding, result_note) = load_epiphany_reorient_result_snapshot(
thread.epiphany_state.as_ref(),
state_db_ctx.as_ref(),
runtime_store_path.as_deref(),
EPIPHANY_REORIENT_LAUNCH_BINDING_ID,
)
.await;
@ -5975,10 +6005,14 @@ impl CodexMessageProcessor {
};
let state_revision = Some(state.revision);
let state_db_ctx = if let Some(loaded_thread) = loaded_thread.as_ref() {
loaded_thread.epiphany_state_runtime().await
let (state_db_ctx, runtime_store_path) = if let Some(loaded_thread) = loaded_thread.as_ref()
{
(
loaded_thread.epiphany_state_runtime().await,
Some(loaded_thread.epiphany_runtime_spine_store_path().await),
)
} else {
None
(None, None)
};
let job_resolution =
load_epiphany_job_launcher_snapshots(state_db_ctx.as_ref(), Some(state)).await;
@ -5986,156 +6020,13 @@ impl CodexMessageProcessor {
.into_iter()
.find(|job| job.id == binding_id);
let Some(binding) = state
.job_bindings
.iter()
.find(|binding| binding.id == binding_id)
else {
self.outgoing
.send_response(
request_id,
ThreadEpiphanyReorientResultResponse {
thread_id: thread_uuid.to_string(),
source,
state_status: ThreadEpiphanyReorientStateStatus::Ready,
state_revision,
binding_id,
status: ThreadEpiphanyReorientResultStatus::MissingBinding,
job,
finding: None,
note: "No matching Epiphany reorientation worker binding exists."
.to_string(),
},
)
.await;
return;
};
let Some(agent_job_id) = binding_agent_jobs_job_id(binding) else {
self.outgoing
.send_response(
request_id,
ThreadEpiphanyReorientResultResponse {
thread_id: thread_uuid.to_string(),
source,
state_status: ThreadEpiphanyReorientStateStatus::Ready,
state_revision,
binding_id,
status: ThreadEpiphanyReorientResultStatus::BackendUnavailable,
job,
finding: None,
note: "The matching binding is not currently backed by the agent_jobs backend."
.to_string(),
},
)
.await;
return;
};
let Some(state_db_ctx) = state_db_ctx.as_ref() else {
self.outgoing
.send_response(
request_id,
ThreadEpiphanyReorientResultResponse {
thread_id: thread_uuid.to_string(),
source,
state_status: ThreadEpiphanyReorientStateStatus::Ready,
state_revision,
binding_id,
status: ThreadEpiphanyReorientResultStatus::BackendUnavailable,
job,
finding: None,
note: "State runtime is unavailable, so the bound agent_jobs result cannot be read."
.to_string(),
},
)
.await;
return;
};
let agent_job = match state_db_ctx.get_agent_job(agent_job_id).await {
Ok(Some(job)) => job,
Ok(None) => {
self.outgoing
.send_response(
request_id,
ThreadEpiphanyReorientResultResponse {
thread_id: thread_uuid.to_string(),
source,
state_status: ThreadEpiphanyReorientStateStatus::Ready,
state_revision,
binding_id,
status: ThreadEpiphanyReorientResultStatus::BackendMissing,
job,
finding: None,
note: format!(
"Bound agent_jobs backend job {:?} was not found.",
agent_job_id
),
},
)
.await;
return;
}
Err(err) => {
self.send_internal_error(
request_id,
format!("failed to read Epiphany reorientation backend job: {err}"),
)
.await;
return;
}
};
let item = match state_db_ctx
.get_agent_job_item(agent_job_id, binding_id.as_str())
.await
{
Ok(item) => item,
Err(err) => {
self.send_internal_error(
request_id,
format!("failed to read Epiphany reorientation backend job item: {err}"),
)
.await;
return;
}
};
let Some(item) = item else {
self.outgoing
.send_response(
request_id,
ThreadEpiphanyReorientResultResponse {
thread_id: thread_uuid.to_string(),
source,
state_status: ThreadEpiphanyReorientStateStatus::Ready,
state_revision,
binding_id,
status: ThreadEpiphanyReorientResultStatus::BackendMissing,
job,
finding: None,
note:
"Bound agent_jobs backend item was not found for this reorient worker."
.to_string(),
},
)
.await;
return;
};
let status = map_epiphany_reorient_result_status(agent_job.status, item.status);
let finding = item.result_json.as_ref().map(|result| {
map_epiphany_reorient_finding(
result.clone(),
agent_job.last_error.clone(),
item.last_error.clone(),
)
});
let note = render_epiphany_reorient_result_note(
status,
finding.as_ref(),
item.last_error.as_deref(),
);
let (status, finding, note) = load_epiphany_reorient_result_snapshot(
Some(state),
state_db_ctx.as_ref(),
runtime_store_path.as_deref(),
binding_id.as_str(),
)
.await;
self.outgoing
.send_response(
@ -14430,9 +14321,184 @@ fn render_epiphany_reorient_result_note(
}
}
fn load_epiphany_role_result_from_runtime_spine(
binding: &EpiphanyJobBinding,
runtime_store_path: Option<&Path>,
role_id: ThreadEpiphanyRoleId,
) -> (
ThreadEpiphanyRoleResultStatus,
Option<ThreadEpiphanyRoleFinding>,
String,
) {
let Some(job_id) = binding_backend_job_id(binding) else {
return (
ThreadEpiphanyRoleResultStatus::BackendMissing,
None,
"Heartbeat binding has no runtime-spine job id.".to_string(),
);
};
let Some(runtime_store_path) = runtime_store_path else {
return (
ThreadEpiphanyRoleResultStatus::Pending,
None,
"Heartbeat activation owns this role specialist; no loaded runtime-spine store is available yet."
.to_string(),
);
};
let snapshot = match runtime_job_snapshot(runtime_store_path, job_id) {
Ok(Some(snapshot)) => snapshot,
Ok(None) => {
return (
ThreadEpiphanyRoleResultStatus::Pending,
None,
format!(
"Heartbeat runtime job {:?} has not reported typed state yet.",
job_id
),
);
}
Err(err) => {
return (
ThreadEpiphanyRoleResultStatus::BackendUnavailable,
None,
format!(
"Failed to read heartbeat runtime-spine job {:?}: {err}",
job_id
),
);
}
};
let status = map_runtime_role_result_status(&snapshot);
let finding = snapshot.result.as_ref().map(|result| {
map_epiphany_role_finding(role_id, runtime_job_result_to_role_json(result), None, None)
});
let note = render_epiphany_role_result_note(role_id, status, finding.as_ref(), None);
(status, finding, note)
}
fn load_epiphany_reorient_result_from_runtime_spine(
binding: &EpiphanyJobBinding,
runtime_store_path: Option<&Path>,
) -> (
ThreadEpiphanyReorientResultStatus,
Option<ThreadEpiphanyReorientFinding>,
String,
) {
let Some(job_id) = binding_backend_job_id(binding) else {
return (
ThreadEpiphanyReorientResultStatus::BackendMissing,
None,
"Heartbeat binding has no runtime-spine job id.".to_string(),
);
};
let Some(runtime_store_path) = runtime_store_path else {
return (
ThreadEpiphanyReorientResultStatus::Pending,
None,
"Heartbeat activation owns this reorientation worker; no loaded runtime-spine store is available yet."
.to_string(),
);
};
let snapshot = match runtime_job_snapshot(runtime_store_path, job_id) {
Ok(Some(snapshot)) => snapshot,
Ok(None) => {
return (
ThreadEpiphanyReorientResultStatus::Pending,
None,
format!(
"Heartbeat runtime job {:?} has not reported typed state yet.",
job_id
),
);
}
Err(err) => {
return (
ThreadEpiphanyReorientResultStatus::BackendUnavailable,
None,
format!(
"Failed to read heartbeat runtime-spine job {:?}: {err}",
job_id
),
);
}
};
let status = map_runtime_reorient_result_status(&snapshot);
let finding = snapshot.result.as_ref().map(|result| {
map_epiphany_reorient_finding(runtime_job_result_to_reorient_json(result), None, None)
});
let note = render_epiphany_reorient_result_note(status, finding.as_ref(), None);
(status, finding, note)
}
fn map_runtime_role_result_status(
snapshot: &EpiphanyRuntimeJobSnapshot,
) -> ThreadEpiphanyRoleResultStatus {
match snapshot.job.status {
EpiphanyRuntimeJobStatus::Queued => ThreadEpiphanyRoleResultStatus::Pending,
EpiphanyRuntimeJobStatus::Running | EpiphanyRuntimeJobStatus::WaitingForReview => {
ThreadEpiphanyRoleResultStatus::Running
}
EpiphanyRuntimeJobStatus::Completed => {
if snapshot.result.is_some() {
ThreadEpiphanyRoleResultStatus::Completed
} else {
ThreadEpiphanyRoleResultStatus::Pending
}
}
EpiphanyRuntimeJobStatus::Failed => ThreadEpiphanyRoleResultStatus::Failed,
EpiphanyRuntimeJobStatus::Cancelled => ThreadEpiphanyRoleResultStatus::Cancelled,
}
}
fn map_runtime_reorient_result_status(
snapshot: &EpiphanyRuntimeJobSnapshot,
) -> ThreadEpiphanyReorientResultStatus {
match snapshot.job.status {
EpiphanyRuntimeJobStatus::Queued => ThreadEpiphanyReorientResultStatus::Pending,
EpiphanyRuntimeJobStatus::Running | EpiphanyRuntimeJobStatus::WaitingForReview => {
ThreadEpiphanyReorientResultStatus::Running
}
EpiphanyRuntimeJobStatus::Completed => {
if snapshot.result.is_some() {
ThreadEpiphanyReorientResultStatus::Completed
} else {
ThreadEpiphanyReorientResultStatus::Pending
}
}
EpiphanyRuntimeJobStatus::Failed => ThreadEpiphanyReorientResultStatus::Failed,
EpiphanyRuntimeJobStatus::Cancelled => ThreadEpiphanyReorientResultStatus::Cancelled,
}
}
fn runtime_job_result_to_role_json(result: &EpiphanyRuntimeJobResult) -> serde_json::Value {
serde_json::json!({
"verdict": result.verdict,
"summary": result.summary,
"nextSafeMove": result.next_safe_move,
"evidenceIds": result.evidence_refs,
"artifactRefs": result.artifact_refs,
"runtimeResultId": result.result_id,
"runtimeJobId": result.job_id,
})
}
fn runtime_job_result_to_reorient_json(result: &EpiphanyRuntimeJobResult) -> serde_json::Value {
serde_json::json!({
"mode": result.role,
"summary": result.summary,
"nextSafeMove": result.next_safe_move,
"checkpointStillValid": result.verdict != "fail",
"evidenceIds": result.evidence_refs,
"artifactRefs": result.artifact_refs,
"runtimeResultId": result.result_id,
"runtimeJobId": result.job_id,
})
}
async fn load_epiphany_role_result_snapshot(
state: &EpiphanyThreadState,
state_db_ctx: Option<&StateDbHandle>,
runtime_store_path: Option<&Path>,
role_id: ThreadEpiphanyRoleId,
binding_id: &str,
) -> (
@ -14452,12 +14518,7 @@ async fn load_epiphany_role_result_snapshot(
);
};
if binding_backend_kind(binding) == Some(CoreEpiphanyJobBackendKind::Heartbeat) {
return (
ThreadEpiphanyRoleResultStatus::Pending,
None,
"Heartbeat activation owns this role specialist; no accepted typed result has been reported yet."
.to_string(),
);
return load_epiphany_role_result_from_runtime_spine(binding, runtime_store_path, role_id);
}
let Some(agent_job_id) = binding_agent_jobs_job_id(binding) else {
return (
@ -14535,6 +14596,7 @@ async fn load_epiphany_role_result_snapshot(
async fn load_epiphany_reorient_result_snapshot(
state: Option<&EpiphanyThreadState>,
state_db_ctx: Option<&StateDbHandle>,
runtime_store_path: Option<&Path>,
binding_id: &str,
) -> (
ThreadEpiphanyReorientResultStatus,
@ -14554,19 +14616,14 @@ async fn load_epiphany_reorient_result_snapshot(
.find(|binding| binding.id == binding_id);
let (agent_job, item) = if let Some(binding) = binding {
if binding_backend_kind(binding) == Some(CoreEpiphanyJobBackendKind::Heartbeat) {
return (
ThreadEpiphanyReorientResultStatus::Pending,
None,
"Heartbeat activation owns this reorientation worker; no accepted typed result has been reported yet."
.to_string(),
);
return load_epiphany_reorient_result_from_runtime_spine(binding, runtime_store_path);
}
let Some(agent_job_id) = binding_agent_jobs_job_id(binding) else {
return (
ThreadEpiphanyReorientResultStatus::BackendUnavailable,
None,
"The matching binding is not currently backed by the agent_jobs backend."
.to_string(),
.to_string(),
);
};
let Some(state_db_ctx) = state_db_ctx else {
@ -15665,6 +15722,31 @@ async fn load_completed_epiphany_reorient_finding(
state: &EpiphanyThreadState,
binding_id: &str,
) -> CodexResult<ThreadEpiphanyReorientFinding> {
if let Some(binding) = state
.job_bindings
.iter()
.find(|binding| binding.id == binding_id)
&& binding_backend_kind(binding) == Some(CoreEpiphanyJobBackendKind::Heartbeat)
{
let runtime_store_path = thread.epiphany_runtime_spine_store_path().await;
let (status, finding, _note) = load_epiphany_reorient_result_from_runtime_spine(
binding,
Some(runtime_store_path.as_path()),
);
if status != ThreadEpiphanyReorientResultStatus::Completed {
return Err(CodexErr::InvalidRequest(format!(
"cannot accept reorientation result while worker status is {:?}",
status
)));
}
return finding.ok_or_else(|| {
CodexErr::InvalidRequest(
"cannot accept completed reorientation worker because no typed runtime-spine result was recorded"
.to_string(),
)
});
}
let state_db_ctx = thread.epiphany_state_runtime().await.ok_or_else(|| {
CodexErr::InvalidRequest(
"sqlite state db is unavailable for Epiphany reorientation result acceptance"
@ -15759,6 +15841,27 @@ async fn load_completed_epiphany_role_finding(
binding_id
))
})?;
if binding_backend_kind(binding) == Some(CoreEpiphanyJobBackendKind::Heartbeat) {
let runtime_store_path = thread.epiphany_runtime_spine_store_path().await;
let (status, finding, _note) = load_epiphany_role_result_from_runtime_spine(
binding,
Some(runtime_store_path.as_path()),
role_id,
);
if status != ThreadEpiphanyRoleResultStatus::Completed {
return Err(CodexErr::InvalidRequest(format!(
"cannot accept role result while worker status is {:?}",
status
)));
}
return finding.ok_or_else(|| {
CodexErr::InvalidRequest(
"cannot accept completed role worker because no typed runtime-spine result was recorded"
.to_string(),
)
});
}
let agent_job_id = binding_agent_jobs_job_id(binding).ok_or_else(|| {
CodexErr::InvalidRequest(format!(
"epiphany role binding {:?} is not backed by agent_jobs",
@ -17618,6 +17721,7 @@ pub(crate) async fn maybe_run_epiphany_coordinator_automation_for_turn_boundary(
.await;
let token_usage_info = thread.token_usage_info().await;
let state_db_ctx = thread.epiphany_state_runtime().await;
let runtime_store_path = thread.epiphany_runtime_spine_store_path().await;
let (_state_revision, retrieval, graph, watcher) = map_epiphany_freshness(
Some(&state),
@ -17641,6 +17745,7 @@ pub(crate) async fn maybe_run_epiphany_coordinator_automation_for_turn_boundary(
let (reorient_result_status, reorient_finding, _) = load_epiphany_reorient_result_snapshot(
Some(&state),
state_db_ctx.as_ref(),
Some(runtime_store_path.as_path()),
EPIPHANY_REORIENT_LAUNCH_BINDING_ID,
)
.await;
@ -17669,6 +17774,7 @@ pub(crate) async fn maybe_run_epiphany_coordinator_automation_for_turn_boundary(
let (modeling_result_status, modeling_finding, _) = load_epiphany_role_result_snapshot(
&state,
state_db_ctx.as_ref(),
Some(runtime_store_path.as_path()),
ThreadEpiphanyRoleId::Modeling,
EPIPHANY_MODELING_ROLE_BINDING_ID,
)
@ -17682,6 +17788,7 @@ pub(crate) async fn maybe_run_epiphany_coordinator_automation_for_turn_boundary(
let (verification_result_status, verification_finding, _) = load_epiphany_role_result_snapshot(
&state,
state_db_ctx.as_ref(),
Some(runtime_store_path.as_path()),
ThreadEpiphanyRoleId::Verification,
EPIPHANY_VERIFICATION_ROLE_BINDING_ID,
)
@ -23318,6 +23425,85 @@ mod tests {
);
}
#[test]
fn load_epiphany_role_result_reads_heartbeat_runtime_spine_result() {
let temp = tempfile::tempdir().expect("tempdir");
let store = temp.path().join("runtime.msgpack");
epiphany_core::initialize_runtime_spine(
&store,
epiphany_core::RuntimeSpineInitOptions {
runtime_id: "epiphany-test".to_string(),
display_name: "Epiphany Test".to_string(),
created_at: "2026-05-06T00:00:00Z".to_string(),
},
)
.expect("runtime identity");
epiphany_core::create_runtime_session(
&store,
epiphany_core::RuntimeSpineSessionOptions {
session_id: "epiphany-main".to_string(),
objective: "Read typed runtime results.".to_string(),
created_at: "2026-05-06T00:01:00Z".to_string(),
coordinator_note: "test".to_string(),
},
)
.expect("runtime session");
epiphany_core::create_runtime_job(
&store,
epiphany_core::RuntimeSpineJobOptions {
job_id: "heartbeat-job-1".to_string(),
session_id: "epiphany-main".to_string(),
role: "epiphany-modeler".to_string(),
created_at: "2026-05-06T00:02:00Z".to_string(),
summary: "queued".to_string(),
artifact_refs: Vec::new(),
},
)
.expect("runtime job");
epiphany_core::complete_runtime_job(
&store,
epiphany_core::RuntimeSpineJobResultOptions {
result_id: "heartbeat-result-1".to_string(),
job_id: "heartbeat-job-1".to_string(),
completed_at: "2026-05-06T00:03:00Z".to_string(),
verdict: "pass".to_string(),
summary: "Runtime model ready.".to_string(),
next_safe_move: "Launch verification.".to_string(),
evidence_refs: vec!["evidence:model".to_string()],
artifact_refs: vec!["artifact:model".to_string()],
},
)
.expect("runtime result");
let binding = codex_protocol::protocol::EpiphanyJobBinding {
id: "modeling-checkpoint-worker".to_string(),
kind: codex_protocol::protocol::EpiphanyJobKind::Specialist,
scope: "role-scoped modeling/checkpoint maintenance".to_string(),
owner_role: "epiphany-modeler".to_string(),
launcher_job_id: Some("epiphany-heartbeat-launch-1".to_string()),
authority_scope: Some("epiphany.role.modeling".to_string()),
backend_kind: Some(codex_protocol::protocol::EpiphanyJobBackendKind::Heartbeat),
backend_job_id: Some("heartbeat-job-1".to_string()),
runtime_agent_job_id: None,
linked_subgoal_ids: Vec::new(),
linked_graph_node_ids: Vec::new(),
progress_note: None,
blocking_reason: None,
};
let (status, finding, note) = load_epiphany_role_result_from_runtime_spine(
&binding,
Some(store.as_path()),
ThreadEpiphanyRoleId::Modeling,
);
assert_eq!(status, ThreadEpiphanyRoleResultStatus::Completed);
let finding = finding.expect("runtime finding");
assert_eq!(finding.verdict.as_deref(), Some("pass"));
assert_eq!(finding.summary.as_deref(), Some("Runtime model ready."));
assert_eq!(finding.evidence_ids, vec!["evidence:model".to_string()]);
assert!(note.contains("Modeling role specialist completed"));
}
#[test]
fn map_epiphany_jobs_treats_stale_active_backend_job_as_completed() {
let state = codex_protocol::protocol::EpiphanyThreadState {

View file

@ -56,6 +56,12 @@ use codex_protocol::protocol::TokenUsageInfo;
use codex_protocol::protocol::W3cTraceContext;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use epiphany_core::RuntimeSpineInitOptions;
use epiphany_core::RuntimeSpineJobOptions;
use epiphany_core::RuntimeSpineSessionOptions;
use epiphany_core::create_runtime_job;
use epiphany_core::ensure_runtime_session;
use epiphany_core::initialize_runtime_spine;
use rmcp::model::ReadResourceRequestParams;
use std::collections::HashMap;
use std::collections::HashSet;
@ -517,6 +523,13 @@ impl CodexThread {
let launcher_job_id = format!("epiphany-heartbeat-launch-{}", Uuid::new_v4());
let backend_job_id = Uuid::new_v4().to_string();
let runtime_store = self.epiphany_runtime_spine_store_path().await;
open_epiphany_runtime_spine_job(
runtime_store.as_path(),
&current_state,
&request,
backend_job_id.as_str(),
)?;
let replacement_binding = build_epiphany_job_launch_binding(
&request,
launcher_job_id.as_str(),
@ -547,8 +560,7 @@ impl CodexThread {
job_bindings: Some(next_job_bindings),
..Default::default()
})
.await
?;
.await?;
Ok(EpiphanyJobLaunchResult {
epiphany_state,
@ -689,6 +701,15 @@ impl CodexThread {
})
}
pub async fn epiphany_runtime_spine_store_path(&self) -> PathBuf {
self.config_snapshot()
.await
.cwd
.join("state")
.join("runtime-spine.msgpack")
.to_path_buf()
}
pub async fn epiphany_index(
&self,
force_full_rebuild: bool,
@ -811,8 +832,7 @@ fn validate_epiphany_job_launch_request(request: &EpiphanyJobLaunchRequest) -> C
}
if request.kind != EpiphanyJobKind::Specialist {
return Err(CodexErr::InvalidRequest(
"epiphany job launch currently supports only specialist heartbeat turns"
.to_string(),
"epiphany job launch currently supports only specialist heartbeat turns".to_string(),
));
}
if request.scope.trim().is_empty() {
@ -897,6 +917,70 @@ fn build_epiphany_job_launch_binding(
}
}
fn open_epiphany_runtime_spine_job(
store_path: &std::path::Path,
state: &EpiphanyThreadState,
request: &EpiphanyJobLaunchRequest,
backend_job_id: &str,
) -> CodexResult<()> {
let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
initialize_runtime_spine(
store_path,
RuntimeSpineInitOptions {
runtime_id: "epiphany-local".to_string(),
display_name: "Epiphany Local".to_string(),
created_at: now.clone(),
},
)
.map_err(|err| {
CodexErr::Fatal(format!(
"failed to initialize Epiphany runtime spine {}: {err}",
store_path.display()
))
})?;
ensure_runtime_session(
store_path,
RuntimeSpineSessionOptions {
session_id: "epiphany-main".to_string(),
objective: state
.objective
.clone()
.filter(|objective| !objective.trim().is_empty())
.unwrap_or_else(|| "Epiphany heartbeat activation".to_string()),
created_at: now.clone(),
coordinator_note: "App-server launch opened this typed runtime session.".to_string(),
},
)
.map_err(|err| {
CodexErr::Fatal(format!(
"failed to ensure Epiphany runtime session in {}: {err}",
store_path.display()
))
})?;
create_runtime_job(
store_path,
RuntimeSpineJobOptions {
job_id: backend_job_id.to_string(),
session_id: "epiphany-main".to_string(),
role: request.owner_role.clone(),
created_at: now,
summary: format!(
"Heartbeat activation queued for binding {} with authority {}.",
request.binding_id, request.authority_scope
),
artifact_refs: Vec::new(),
},
)
.map_err(|err| {
CodexErr::Fatal(format!(
"failed to open Epiphany runtime job {:?} in {}: {err}",
backend_job_id,
store_path.display()
))
})?;
Ok(())
}
fn replace_or_append_epiphany_job_binding(
mut bindings: Vec<EpiphanyJobBinding>,
replacement: EpiphanyJobBinding,
@ -1963,7 +2047,10 @@ mod epiphany_update_tests {
let binding =
build_epiphany_job_launch_binding(&request, "epiphany-heartbeat-launch-1", "turn-1");
assert_eq!(binding.backend_kind, Some(EpiphanyJobBackendKind::Heartbeat));
assert_eq!(
binding.backend_kind,
Some(EpiphanyJobBackendKind::Heartbeat)
);
assert_eq!(binding.backend_job_id.as_deref(), Some("turn-1"));
assert_eq!(binding.runtime_agent_job_id, None);
assert!(