Merge Face and organ contract work

This commit is contained in:
Meta Vens 2026-05-27 14:22:27 +01:00
commit 08c258c977
140 changed files with 14423 additions and 5441 deletions

2
.gitignore vendored
View file

@ -18,6 +18,8 @@ apps/epiphany-gui/src-tauri/target/
.epiphany-heartbeats/
.epiphany-face/
.epiphany-rumination/
.epiphany-run/
.voidbot/
state/agent-heartbeats.json
state/*.msgpack.lock
state/*.projection.json

View file

@ -12,6 +12,38 @@ typed, inspectable surfaces: maps, evidence, retrieval state, graph/frontier
state, churn pressure, and compact client reflections. The goal is not maximal
output. The goal is a machine whose parts visibly deserve to exist.
## Project Direction
Epiphany is moving toward project-native agency.
The user should not have to write a perfect brief, name every subsystem, and
pre-plan the code flow for the whole architecture before the machine can help.
That is just outsourcing project management to the human and calling the agent
smart because it waited politely.
The intended shape is simpler and stranger: you talk to the project.
Each Epiphany owns one or more repos, carries durable memory and jurisdiction
for them, and can grow one or more Faces. A Face is the public mouth and
situated personality of that project: visible in Aquarium, addressable in
Discord, suitable for voice or stream presence, and backed by the same typed
state, heartbeat initiative, repo map, evidence, and review gates as the rest of
the swarm.
VoidBot is already showing the small version: repo identities such as Nibu,
Aqua, and Mimir have Discord roles, repo-local Face state, proposal authority,
pending mentions, and heartbeat turns. Epiphany is the larger native substrate:
projects should notice pressure, ruminate, ask questions, schedule modeling or
verification, advocate for their own repo needs, and structure work without
requiring the human to spell out the implementation graph first.
Aquarium is the most direct client because it can show the swarm, its Faces,
its heartbeat, its memories, and its decisions. It is not the only mouth. A
project should also be reachable through Discord, voice/WebRTC surfaces, stream
overlays, native CLIs, and CultNet-speaking tools. The interface contract is
not "prompt an agent." It is "speak to the project; inspect how it thinks; grant
or refuse authority when it wants to act."
## What Exists Now
The current spine is real, not aspirational:
@ -35,27 +67,67 @@ The current spine is real, not aspirational:
- read-only reflection surfaces through `thread/epiphany/scene`, `jobs`, `freshness`, `context`, `pressure`, and `reorient`
- a first dogfood CLI operator view through `tools/epiphany_mvp_status.py`, including explicit implementation, modeling/checkpoint, verification/review, and reorientation lanes
- durable investigation checkpointing for compaction-safe planning
- Ghostlight/VoidBot-derived heartbeat initiative with heat, active-turn
freeze, idle rumination, sleep/dream maintenance, and Face as a first-class
public-surface participant
- repo personality/birth initialization that can seed role dossiers and
heartbeat pressure from repo terrain before a project starts speaking
- CultNet schema contracts for runtime, heartbeat, Face, character-turn,
Discord-persona, repo-initialization, Rider, Unity, and operator surfaces
- repo-owned heavy Epiphany organs in `epiphany-core/`, with vendored Codex kept as the host seam where practical
Phase 1 through Phase 6 are landed enough for the current experiment. The open
questions are now about observability, invalidation, coordination, authority,
and safe capability growth, not whether typed state can exist at all.
## Run Locally
The current one-command operator path is:
```powershell
.\tools\epiphany_local_run.ps1
```
Default mode is `smoke`: it builds the retained Codex app-server compatibility
edge, builds the native Epiphany operator binaries, runs the coordinator smoke,
and writes launcher artifacts under `.epiphany-run/` plus coordinator evidence
under `.epiphany-dogfood/`.
Useful variants:
```powershell
.\tools\epiphany_local_run.ps1 -Mode status
.\tools\epiphany_local_run.ps1 -Mode plan
.\tools\epiphany_local_run.ps1 -Mode run -MaxSteps 4
.\tools\epiphany_local_run.ps1 -Mode status -ThreadId <id> -Workspace E:\Projects\SomeRepo
```
`run` is the live coordinator loop. It also builds `epiphany-openai-runtime` and
uses the retained Codex auth/model transport spine. `status`, `plan`, and
`smoke` do not spend model calls.
## Where This Leads
The architectural goal is not one giant agent that does archaeology, design,
patching, and self-grading in a single token fog. The goal is a maintained
control system:
patching, and self-grading in a single token fog. It is also not a chat box
where the human must already know the solution architecture before asking.
The goal is a maintained project organism:
- graph and source-modeling agents keep the machine map current
- freshness, watcher input, pressure, and reorientation signals decide whether that map still deserves trust
- verifier agents check claims, diffs, and outcomes against reality
- coding agents work from bounded packets of graph context, evidence, code refs, and local source reads instead of re-spelunking the repo from scratch every turn
- Faces translate project state into conversation, voice, stream presence, and
Discord/Aquarium discussion without becoming omniscient hidden operators
- initiative scheduling decides which repo, Face, or internal organ should
think next, and heat lets the operator turn up one agent, one project, one
group, or the whole swarm
That still leaves direct source reading where it belongs: at the last mile,
when a patch has to be grounded in the real files. The difference is that broad
exploration, coherence maintenance, and verification stop living in the same
overworked skull.
exploration, coherence maintenance, social interface, scheduling, and
verification stop living in the same overworked skull.
## Repo Tour

View file

@ -6,3 +6,14 @@ The Aquarium operator UI moved to its own sibling repository:
EpiphanyAgent remains the harness, typed-state, coordinator, bridge, and backend
repo. Aquarium is the Tauri/React/WebGL interface organism over those surfaces.
Aquarium is the primary client for the next public shape of Epiphany: projects
with Faces. A user should be able to talk to a project, not assemble a perfect
agent prompt. Aquarium should show the repo's Face, heartbeat, memory, pending
turns, current jurisdiction, proposed actions, and evidence so the human can
converse, inspect, approve, refuse, or raise heat without pre-writing the whole
architecture brief.
Discord roles, WebRTC/voice surfaces, stream overlays, native CLIs, and other
CultNet clients are sibling mouths over the same substrate. Aquarium is the
clearest window, not the only door.

View file

@ -13,15 +13,20 @@ path = "src/lib.rs"
anyhow = "1"
chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] }
codex-app-server-protocol = { path = "../vendor/codex/codex-rs/app-server-protocol" }
codex-core = { path = "../vendor/codex/codex-rs/core" }
codex-protocol = { path = "../vendor/codex/codex-rs/protocol" }
codex-utils-absolute-path = { path = "../vendor/codex/codex-rs/utils/absolute-path" }
epiphany-core = { path = "../epiphany-core" }
epiphany-openai-adapter = { path = "../epiphany-openai-adapter" }
epiphany-openai-codex-spine = { path = "../epiphany-openai-codex-spine" }
epiphany-state-model = { path = "../epiphany-state-model" }
notify = "8.2.0"
serde_json = "1"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["rt", "sync"] }
toml = "0.9"
tracing = "0.1"
uuid = { version = "1", features = ["v4"] }
[patch.crates-io]
tokio-tungstenite = { git = "https://github.com/openai-oss-forks/tokio-tungstenite", rev = "132f5b39c862e3a970f731d709608b3e6276d5f6" }
tungstenite = { git = "https://github.com/openai-oss-forks/tungstenite-rs", rev = "9200079d3b54a1ff51072e24d81fd354f085156f" }
[patch."ssh://git@github.com/openai-oss-forks/tungstenite-rs.git"]
tungstenite = { git = "https://github.com/openai-oss-forks/tungstenite-rs", rev = "9200079d3b54a1ff51072e24d81fd354f085156f" }

View file

@ -1,258 +1,27 @@
use codex_app_server_protocol::ThreadEpiphanyContext;
use codex_app_server_protocol::ThreadEpiphanyContextMissing;
use codex_app_server_protocol::ThreadEpiphanyContextParams;
use codex_app_server_protocol::ThreadEpiphanyContextStateStatus;
use codex_app_server_protocol::ThreadEpiphanyGraphContext;
use codex_app_server_protocol::ThreadEpiphanyGraphQuery;
use codex_app_server_protocol::ThreadEpiphanyGraphQueryDirection;
use codex_app_server_protocol::ThreadEpiphanyGraphQueryKind;
use codex_app_server_protocol::ThreadEpiphanyGraphQueryMatched;
use codex_app_server_protocol::ThreadEpiphanyGraphQueryMissing;
use codex_app_server_protocol::ThreadEpiphanyPlanningSummary;
use codex_protocol::protocol::EpiphanyGraphCheckpoint;
use codex_protocol::protocol::EpiphanyGraphFrontier;
use codex_protocol::protocol::EpiphanyPlanningState;
use codex_protocol::protocol::EpiphanyThreadState;
use epiphany_core::EpiphanyContext as CoreEpiphanyContext;
use epiphany_core::EpiphanyContextMissing as CoreEpiphanyContextMissing;
use epiphany_core::EpiphanyContextParams as CoreEpiphanyContextParams;
use epiphany_core::EpiphanyContextStateStatus as CoreEpiphanyContextStateStatus;
use epiphany_core::EpiphanyContextParams;
use epiphany_core::EpiphanyContextView;
use epiphany_core::EpiphanyGraphContext as CoreEpiphanyGraphContext;
use epiphany_core::EpiphanyGraphQuery as CoreEpiphanyGraphQuery;
use epiphany_core::EpiphanyGraphQueryDirection as CoreEpiphanyGraphQueryDirection;
use epiphany_core::EpiphanyGraphQueryKind as CoreEpiphanyGraphQueryKind;
use epiphany_core::EpiphanyGraphQueryMatched as CoreEpiphanyGraphQueryMatched;
use epiphany_core::EpiphanyGraphQueryMissing as CoreEpiphanyGraphQueryMissing;
use epiphany_core::EpiphanyGraphQuery;
use epiphany_core::EpiphanyGraphQueryView;
use epiphany_core::EpiphanyPlanningStateStatus as CoreEpiphanyPlanningStateStatus;
use epiphany_core::EpiphanyPlanningSummary as CoreEpiphanyPlanningSummary;
use epiphany_core::EpiphanyPlanningView;
use epiphany_core::derive_context;
use epiphany_core::derive_graph_query;
use epiphany_core::derive_planning_view;
use epiphany_state_model::EpiphanyThreadState;
pub fn map_epiphany_context(
pub fn derive_epiphany_context(
state: Option<&EpiphanyThreadState>,
params: &ThreadEpiphanyContextParams,
) -> (
ThreadEpiphanyContextStateStatus,
Option<u64>,
ThreadEpiphanyContext,
ThreadEpiphanyContextMissing,
) {
map_core_epiphany_context_view(derive_context(
state,
&CoreEpiphanyContextParams {
graph_node_ids: params.graph_node_ids.clone(),
graph_edge_ids: params.graph_edge_ids.clone(),
observation_ids: params.observation_ids.clone(),
evidence_ids: params.evidence_ids.clone(),
include_active_frontier: params.include_active_frontier,
include_linked_evidence: params.include_linked_evidence,
},
))
params: &EpiphanyContextParams,
) -> EpiphanyContextView {
derive_context(state, params)
}
fn map_core_epiphany_context_view(
view: EpiphanyContextView,
) -> (
ThreadEpiphanyContextStateStatus,
Option<u64>,
ThreadEpiphanyContext,
ThreadEpiphanyContextMissing,
) {
(
map_core_epiphany_context_state_status(view.state_status),
view.state_revision,
map_core_epiphany_context(view.context),
map_core_epiphany_context_missing(view.missing),
)
pub fn derive_epiphany_planning(state: Option<&EpiphanyThreadState>) -> EpiphanyPlanningView {
derive_planning_view(state)
}
fn map_core_epiphany_context_state_status(
status: CoreEpiphanyContextStateStatus,
) -> ThreadEpiphanyContextStateStatus {
match status {
CoreEpiphanyContextStateStatus::Missing => ThreadEpiphanyContextStateStatus::Missing,
CoreEpiphanyContextStateStatus::Ready => ThreadEpiphanyContextStateStatus::Ready,
}
}
fn map_core_epiphany_context(context: CoreEpiphanyContext) -> ThreadEpiphanyContext {
ThreadEpiphanyContext {
graph: map_core_epiphany_graph_context(context.graph),
frontier: context.frontier,
checkpoint: context.checkpoint,
investigation_checkpoint: context.investigation_checkpoint,
observations: context.observations,
evidence: context.evidence,
}
}
fn map_core_epiphany_graph_context(graph: CoreEpiphanyGraphContext) -> ThreadEpiphanyGraphContext {
ThreadEpiphanyGraphContext {
architecture_nodes: graph.architecture_nodes,
architecture_edges: graph.architecture_edges,
dataflow_nodes: graph.dataflow_nodes,
dataflow_edges: graph.dataflow_edges,
links: graph.links,
}
}
fn map_core_epiphany_context_missing(
missing: CoreEpiphanyContextMissing,
) -> ThreadEpiphanyContextMissing {
ThreadEpiphanyContextMissing {
graph_node_ids: missing.graph_node_ids,
graph_edge_ids: missing.graph_edge_ids,
observation_ids: missing.observation_ids,
evidence_ids: missing.evidence_ids,
}
}
pub fn map_epiphany_planning(
pub fn derive_epiphany_graph_query(
state: Option<&EpiphanyThreadState>,
) -> (
ThreadEpiphanyContextStateStatus,
Option<u64>,
EpiphanyPlanningState,
ThreadEpiphanyPlanningSummary,
) {
let view = derive_planning_view(state);
map_core_epiphany_planning_view(view)
}
fn map_core_epiphany_planning_view(
view: EpiphanyPlanningView,
) -> (
ThreadEpiphanyContextStateStatus,
Option<u64>,
EpiphanyPlanningState,
ThreadEpiphanyPlanningSummary,
) {
(
match view.state_status {
CoreEpiphanyPlanningStateStatus::Missing => ThreadEpiphanyContextStateStatus::Missing,
CoreEpiphanyPlanningStateStatus::Ready => ThreadEpiphanyContextStateStatus::Ready,
},
view.state_revision,
view.planning,
map_core_epiphany_planning_summary(view.summary),
)
}
fn map_core_epiphany_planning_summary(
summary: CoreEpiphanyPlanningSummary,
) -> ThreadEpiphanyPlanningSummary {
ThreadEpiphanyPlanningSummary {
capture_count: summary.capture_count,
pending_capture_count: summary.pending_capture_count,
github_issue_capture_count: summary.github_issue_capture_count,
backlog_item_count: summary.backlog_item_count,
ready_backlog_item_count: summary.ready_backlog_item_count,
roadmap_stream_count: summary.roadmap_stream_count,
objective_draft_count: summary.objective_draft_count,
draft_objective_count: summary.draft_objective_count,
active_objective: summary.active_objective,
note: summary.note,
}
}
pub fn map_epiphany_graph_query(
state: Option<&EpiphanyThreadState>,
query: &ThreadEpiphanyGraphQuery,
) -> (
ThreadEpiphanyContextStateStatus,
Option<u64>,
ThreadEpiphanyGraphContext,
Option<EpiphanyGraphFrontier>,
Option<EpiphanyGraphCheckpoint>,
ThreadEpiphanyGraphQueryMatched,
ThreadEpiphanyGraphQueryMissing,
) {
map_core_epiphany_graph_query_view(derive_graph_query(
state,
&map_core_epiphany_graph_query(query),
))
}
fn map_core_epiphany_graph_query(query: &ThreadEpiphanyGraphQuery) -> CoreEpiphanyGraphQuery {
CoreEpiphanyGraphQuery {
kind: map_core_epiphany_graph_query_kind(query.kind),
node_ids: query.node_ids.clone(),
edge_ids: query.edge_ids.clone(),
paths: query.paths.clone(),
symbols: query.symbols.clone(),
edge_kinds: query.edge_kinds.clone(),
direction: query.direction.map(map_core_epiphany_graph_query_direction),
depth: query.depth,
include_links: query.include_links,
}
}
fn map_core_epiphany_graph_query_kind(
kind: ThreadEpiphanyGraphQueryKind,
) -> CoreEpiphanyGraphQueryKind {
match kind {
ThreadEpiphanyGraphQueryKind::Node => CoreEpiphanyGraphQueryKind::Node,
ThreadEpiphanyGraphQueryKind::Path => CoreEpiphanyGraphQueryKind::Path,
ThreadEpiphanyGraphQueryKind::FrontierNeighborhood => {
CoreEpiphanyGraphQueryKind::FrontierNeighborhood
}
ThreadEpiphanyGraphQueryKind::Neighbors => CoreEpiphanyGraphQueryKind::Neighbors,
}
}
fn map_core_epiphany_graph_query_direction(
direction: ThreadEpiphanyGraphQueryDirection,
) -> CoreEpiphanyGraphQueryDirection {
match direction {
ThreadEpiphanyGraphQueryDirection::Incoming => CoreEpiphanyGraphQueryDirection::Incoming,
ThreadEpiphanyGraphQueryDirection::Outgoing => CoreEpiphanyGraphQueryDirection::Outgoing,
ThreadEpiphanyGraphQueryDirection::Both => CoreEpiphanyGraphQueryDirection::Both,
}
}
fn map_core_epiphany_graph_query_view(
view: EpiphanyGraphQueryView,
) -> (
ThreadEpiphanyContextStateStatus,
Option<u64>,
ThreadEpiphanyGraphContext,
Option<EpiphanyGraphFrontier>,
Option<EpiphanyGraphCheckpoint>,
ThreadEpiphanyGraphQueryMatched,
ThreadEpiphanyGraphQueryMissing,
) {
(
map_core_epiphany_context_state_status(view.state_status),
view.state_revision,
map_core_epiphany_graph_context(view.graph),
view.frontier,
view.checkpoint,
map_core_epiphany_graph_query_matched(view.matched),
map_core_epiphany_graph_query_missing(view.missing),
)
}
fn map_core_epiphany_graph_query_matched(
matched: CoreEpiphanyGraphQueryMatched,
) -> ThreadEpiphanyGraphQueryMatched {
ThreadEpiphanyGraphQueryMatched {
node_ids: matched.node_ids,
edge_ids: matched.edge_ids,
paths: matched.paths,
symbols: matched.symbols,
edge_kinds: matched.edge_kinds,
}
}
fn map_core_epiphany_graph_query_missing(
missing: CoreEpiphanyGraphQueryMissing,
) -> ThreadEpiphanyGraphQueryMissing {
ThreadEpiphanyGraphQueryMissing {
node_ids: missing.node_ids,
edge_ids: missing.edge_ids,
}
query: &EpiphanyGraphQuery,
) -> EpiphanyGraphQueryView {
derive_graph_query(state, query)
}

View file

@ -0,0 +1,155 @@
use codex_app_server_protocol::ThreadEpiphanyContext;
use codex_app_server_protocol::ThreadEpiphanyContextMissing;
use codex_app_server_protocol::ThreadEpiphanyContextStateStatus;
use codex_app_server_protocol::ThreadEpiphanyGraphContext;
use codex_app_server_protocol::ThreadEpiphanyGraphQueryMatched;
use codex_app_server_protocol::ThreadEpiphanyGraphQueryMissing;
use codex_app_server_protocol::ThreadEpiphanyPlanningSummary;
use epiphany_core::EpiphanyContext as CoreEpiphanyContext;
use epiphany_core::EpiphanyContextMissing as CoreEpiphanyContextMissing;
use epiphany_core::EpiphanyContextStateStatus as CoreEpiphanyContextStateStatus;
use epiphany_core::EpiphanyContextView;
use epiphany_core::EpiphanyGraphContext as CoreEpiphanyGraphContext;
use epiphany_core::EpiphanyGraphQueryMatched as CoreEpiphanyGraphQueryMatched;
use epiphany_core::EpiphanyGraphQueryMissing as CoreEpiphanyGraphQueryMissing;
use epiphany_core::EpiphanyGraphQueryView;
use epiphany_core::EpiphanyPlanningStateStatus as CoreEpiphanyPlanningStateStatus;
use epiphany_core::EpiphanyPlanningSummary as CoreEpiphanyPlanningSummary;
use epiphany_core::EpiphanyPlanningView;
use epiphany_state_model::EpiphanyGraphCheckpoint;
use epiphany_state_model::EpiphanyGraphFrontier;
use epiphany_state_model::EpiphanyPlanningState;
pub fn protocol_context_view(
view: EpiphanyContextView,
) -> (
ThreadEpiphanyContextStateStatus,
Option<u64>,
ThreadEpiphanyContext,
ThreadEpiphanyContextMissing,
) {
(
protocol_context_state_status(view.state_status),
view.state_revision,
protocol_context(view.context),
protocol_context_missing(view.missing),
)
}
pub fn protocol_context_state_status(
status: CoreEpiphanyContextStateStatus,
) -> ThreadEpiphanyContextStateStatus {
match status {
CoreEpiphanyContextStateStatus::Missing => ThreadEpiphanyContextStateStatus::Missing,
CoreEpiphanyContextStateStatus::Ready => ThreadEpiphanyContextStateStatus::Ready,
}
}
fn protocol_context(context: CoreEpiphanyContext) -> ThreadEpiphanyContext {
ThreadEpiphanyContext {
graph: protocol_graph_context(context.graph),
frontier: context.frontier,
checkpoint: context.checkpoint,
investigation_checkpoint: context.investigation_checkpoint,
observations: context.observations,
evidence: context.evidence,
}
}
pub fn protocol_graph_context(graph: CoreEpiphanyGraphContext) -> ThreadEpiphanyGraphContext {
ThreadEpiphanyGraphContext {
architecture_nodes: graph.architecture_nodes,
architecture_edges: graph.architecture_edges,
dataflow_nodes: graph.dataflow_nodes,
dataflow_edges: graph.dataflow_edges,
links: graph.links,
}
}
fn protocol_context_missing(missing: CoreEpiphanyContextMissing) -> ThreadEpiphanyContextMissing {
ThreadEpiphanyContextMissing {
graph_node_ids: missing.graph_node_ids,
graph_edge_ids: missing.graph_edge_ids,
observation_ids: missing.observation_ids,
evidence_ids: missing.evidence_ids,
}
}
pub fn protocol_planning_view(
view: EpiphanyPlanningView,
) -> (
ThreadEpiphanyContextStateStatus,
Option<u64>,
EpiphanyPlanningState,
ThreadEpiphanyPlanningSummary,
) {
(
match view.state_status {
CoreEpiphanyPlanningStateStatus::Missing => ThreadEpiphanyContextStateStatus::Missing,
CoreEpiphanyPlanningStateStatus::Ready => ThreadEpiphanyContextStateStatus::Ready,
},
view.state_revision,
view.planning,
protocol_planning_summary(view.summary),
)
}
fn protocol_planning_summary(
summary: CoreEpiphanyPlanningSummary,
) -> ThreadEpiphanyPlanningSummary {
ThreadEpiphanyPlanningSummary {
capture_count: summary.capture_count,
pending_capture_count: summary.pending_capture_count,
github_issue_capture_count: summary.github_issue_capture_count,
backlog_item_count: summary.backlog_item_count,
ready_backlog_item_count: summary.ready_backlog_item_count,
roadmap_stream_count: summary.roadmap_stream_count,
objective_draft_count: summary.objective_draft_count,
draft_objective_count: summary.draft_objective_count,
active_objective: summary.active_objective,
note: summary.note,
}
}
pub fn protocol_graph_query_view(
view: EpiphanyGraphQueryView,
) -> (
ThreadEpiphanyContextStateStatus,
Option<u64>,
ThreadEpiphanyGraphContext,
Option<EpiphanyGraphFrontier>,
Option<EpiphanyGraphCheckpoint>,
ThreadEpiphanyGraphQueryMatched,
ThreadEpiphanyGraphQueryMissing,
) {
(
protocol_context_state_status(view.state_status),
view.state_revision,
protocol_graph_context(view.graph),
view.frontier,
view.checkpoint,
protocol_graph_query_matched(view.matched),
protocol_graph_query_missing(view.missing),
)
}
fn protocol_graph_query_matched(
matched: CoreEpiphanyGraphQueryMatched,
) -> ThreadEpiphanyGraphQueryMatched {
ThreadEpiphanyGraphQueryMatched {
node_ids: matched.node_ids,
edge_ids: matched.edge_ids,
paths: matched.paths,
symbols: matched.symbols,
edge_kinds: matched.edge_kinds,
}
}
fn protocol_graph_query_missing(
missing: CoreEpiphanyGraphQueryMissing,
) -> ThreadEpiphanyGraphQueryMissing {
ThreadEpiphanyGraphQueryMissing {
node_ids: missing.node_ids,
edge_ids: missing.edge_ids,
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,304 @@
use codex_app_server_protocol::ThreadEpiphanyCoordinatorAction;
use codex_app_server_protocol::ThreadEpiphanyCoordinatorSignals;
use codex_app_server_protocol::ThreadEpiphanyCrrcAction;
use codex_app_server_protocol::ThreadEpiphanyCrrcRecommendation;
use codex_app_server_protocol::ThreadEpiphanyJob;
use codex_app_server_protocol::ThreadEpiphanyReorientAction;
use codex_app_server_protocol::ThreadEpiphanyRoleId;
use codex_app_server_protocol::ThreadEpiphanyRoleLane;
use codex_app_server_protocol::ThreadEpiphanyRoleStatus;
use codex_app_server_protocol::ThreadEpiphanyRolesSource;
use codex_app_server_protocol::ThreadEpiphanySceneAction;
use codex_app_server_protocol::ThreadEpiphanyViewCoordinator;
use epiphany_core::EpiphanyCoordinatorAction as CoreEpiphanyCoordinatorAction;
use epiphany_core::EpiphanyCoordinatorRoleId as CoreEpiphanyCoordinatorRoleId;
use epiphany_core::EpiphanyCoordinatorRoleStatus as CoreEpiphanyCoordinatorRoleStatus;
use epiphany_core::EpiphanyCoordinatorSceneAction as CoreEpiphanyCoordinatorSceneAction;
use epiphany_core::EpiphanyCoordinatorSourceSignals;
use epiphany_core::EpiphanyCrrcAction as CoreEpiphanyCrrcAction;
use epiphany_core::EpiphanyCrrcRecommendation as CoreEpiphanyCrrcRecommendation;
use epiphany_core::EpiphanyCrrcSceneAction as CoreEpiphanyCrrcSceneAction;
use epiphany_core::EpiphanyJobStatus as CoreEpiphanyJobStatus;
use epiphany_core::EpiphanyJobView;
use epiphany_core::EpiphanyReorientAction as CoreEpiphanyReorientAction;
use epiphany_core::EpiphanyReorientStateStatus as CoreEpiphanyReorientStateStatus;
use epiphany_core::EpiphanyRoleBoardJob;
use epiphany_core::EpiphanyRoleBoardJobStatus;
use epiphany_core::EpiphanyRoleBoardLane;
use epiphany_state_model::EpiphanyJobKind as CoreEpiphanyJobKind;
use crate::coordinator::EpiphanyCoordinatorStatus;
use crate::coordinator::EpiphanyRoleBoardStatus;
use crate::protocol_edge::protocol_job_from_surface;
use crate::protocol_edge::protocol_pressure_level;
use crate::protocol_edge::protocol_reorient_result_status;
use crate::protocol_edge::protocol_reorient_state_status;
use crate::protocol_edge::protocol_role_result_status;
fn protocol_coordinator_source_signals(
signals: EpiphanyCoordinatorSourceSignals,
) -> ThreadEpiphanyCoordinatorSignals {
ThreadEpiphanyCoordinatorSignals {
pressure_level: protocol_pressure_level(signals.pressure_level),
should_prepare_compaction: signals.should_prepare_compaction,
reorient_action: protocol_reorient_action(signals.reorient_action),
crrc_action: protocol_crrc_action(signals.crrc_action),
modeling_result_status: protocol_role_result_status(signals.modeling_result_status),
verification_result_status: protocol_role_result_status(signals.verification_result_status),
reorient_result_status: protocol_reorient_result_status(signals.reorient_result_status),
}
}
fn protocol_reorient_action(action: CoreEpiphanyReorientAction) -> ThreadEpiphanyReorientAction {
match action {
CoreEpiphanyReorientAction::Resume => ThreadEpiphanyReorientAction::Resume,
CoreEpiphanyReorientAction::Regather => ThreadEpiphanyReorientAction::Regather,
}
}
pub fn protocol_crrc_action(action: CoreEpiphanyCrrcAction) -> ThreadEpiphanyCrrcAction {
match action {
CoreEpiphanyCrrcAction::Continue => ThreadEpiphanyCrrcAction::Continue,
CoreEpiphanyCrrcAction::PrepareCheckpoint => ThreadEpiphanyCrrcAction::PrepareCheckpoint,
CoreEpiphanyCrrcAction::LaunchReorientWorker => {
ThreadEpiphanyCrrcAction::LaunchReorientWorker
}
CoreEpiphanyCrrcAction::WaitForReorientWorker => {
ThreadEpiphanyCrrcAction::WaitForReorientWorker
}
CoreEpiphanyCrrcAction::ReviewReorientResult => {
ThreadEpiphanyCrrcAction::ReviewReorientResult
}
CoreEpiphanyCrrcAction::AcceptReorientResult => {
ThreadEpiphanyCrrcAction::AcceptReorientResult
}
CoreEpiphanyCrrcAction::RegatherManually => ThreadEpiphanyCrrcAction::RegatherManually,
}
}
fn protocol_crrc_scene_action(action: CoreEpiphanyCrrcSceneAction) -> ThreadEpiphanySceneAction {
match action {
CoreEpiphanyCrrcSceneAction::Update => ThreadEpiphanySceneAction::Update,
CoreEpiphanyCrrcSceneAction::Reorient => ThreadEpiphanySceneAction::Reorient,
CoreEpiphanyCrrcSceneAction::ReorientLaunch => ThreadEpiphanySceneAction::ReorientLaunch,
CoreEpiphanyCrrcSceneAction::ReorientResult => ThreadEpiphanySceneAction::ReorientResult,
CoreEpiphanyCrrcSceneAction::ReorientAccept => ThreadEpiphanySceneAction::ReorientAccept,
}
}
pub fn protocol_crrc_recommendation(
recommendation: CoreEpiphanyCrrcRecommendation,
) -> ThreadEpiphanyCrrcRecommendation {
ThreadEpiphanyCrrcRecommendation {
action: protocol_crrc_action(recommendation.action),
recommended_scene_action: recommendation
.recommended_scene_action
.map(protocol_crrc_scene_action),
reason: recommendation.reason,
}
}
pub fn protocol_coordinator_view(
thread_id: String,
loaded: bool,
state_status: CoreEpiphanyReorientStateStatus,
state_revision: Option<u64>,
status: EpiphanyCoordinatorStatus,
roles: EpiphanyRoleBoardStatus,
) -> ThreadEpiphanyViewCoordinator {
ThreadEpiphanyViewCoordinator {
thread_id,
source: if loaded {
ThreadEpiphanyRolesSource::Live
} else {
ThreadEpiphanyRolesSource::Stored
},
state_status: protocol_reorient_state_status(state_status),
state_revision,
action: protocol_coordinator_action(status.core.decision.action),
target_role: status
.core
.decision
.target_role
.map(protocol_coordinator_role_id),
recommended_scene_action: status
.core
.decision
.recommended_scene_action
.map(protocol_coordinator_scene_action),
requires_review: status.core.decision.requires_review,
can_auto_run: status.core.decision.can_auto_run,
reason: status.core.decision.reason,
source_signals: protocol_coordinator_source_signals(status.core.source_signals),
roles: protocol_role_board_lanes(&roles),
note: status.note,
}
}
fn protocol_coordinator_role_id(role_id: CoreEpiphanyCoordinatorRoleId) -> ThreadEpiphanyRoleId {
match role_id {
CoreEpiphanyCoordinatorRoleId::Implementation => ThreadEpiphanyRoleId::Implementation,
CoreEpiphanyCoordinatorRoleId::Imagination => ThreadEpiphanyRoleId::Imagination,
CoreEpiphanyCoordinatorRoleId::Modeling => ThreadEpiphanyRoleId::Modeling,
CoreEpiphanyCoordinatorRoleId::Verification => ThreadEpiphanyRoleId::Verification,
CoreEpiphanyCoordinatorRoleId::Reorientation => ThreadEpiphanyRoleId::Reorientation,
}
}
fn protocol_coordinator_scene_action(
action: CoreEpiphanyCoordinatorSceneAction,
) -> ThreadEpiphanySceneAction {
match action {
CoreEpiphanyCoordinatorSceneAction::Update => ThreadEpiphanySceneAction::Update,
CoreEpiphanyCoordinatorSceneAction::Reorient => ThreadEpiphanySceneAction::Reorient,
CoreEpiphanyCoordinatorSceneAction::ReorientLaunch => {
ThreadEpiphanySceneAction::ReorientLaunch
}
CoreEpiphanyCoordinatorSceneAction::ReorientResult => {
ThreadEpiphanySceneAction::ReorientResult
}
CoreEpiphanyCoordinatorSceneAction::ReorientAccept => {
ThreadEpiphanySceneAction::ReorientAccept
}
CoreEpiphanyCoordinatorSceneAction::RoleLaunch => ThreadEpiphanySceneAction::RoleLaunch,
CoreEpiphanyCoordinatorSceneAction::RoleResult => ThreadEpiphanySceneAction::RoleResult,
}
}
fn protocol_coordinator_action(
action: CoreEpiphanyCoordinatorAction,
) -> ThreadEpiphanyCoordinatorAction {
match action {
CoreEpiphanyCoordinatorAction::PrepareCheckpoint => {
ThreadEpiphanyCoordinatorAction::PrepareCheckpoint
}
CoreEpiphanyCoordinatorAction::CompactRehydrateReorient => {
ThreadEpiphanyCoordinatorAction::CompactRehydrateReorient
}
CoreEpiphanyCoordinatorAction::LaunchReorientWorker => {
ThreadEpiphanyCoordinatorAction::LaunchReorientWorker
}
CoreEpiphanyCoordinatorAction::WaitForReorientWorker => {
ThreadEpiphanyCoordinatorAction::WaitForReorientWorker
}
CoreEpiphanyCoordinatorAction::ReviewReorientResult => {
ThreadEpiphanyCoordinatorAction::ReviewReorientResult
}
CoreEpiphanyCoordinatorAction::RegatherManually => {
ThreadEpiphanyCoordinatorAction::RegatherManually
}
CoreEpiphanyCoordinatorAction::LaunchModeling => {
ThreadEpiphanyCoordinatorAction::LaunchModeling
}
CoreEpiphanyCoordinatorAction::ReviewModelingResult => {
ThreadEpiphanyCoordinatorAction::ReviewModelingResult
}
CoreEpiphanyCoordinatorAction::LaunchVerification => {
ThreadEpiphanyCoordinatorAction::LaunchVerification
}
CoreEpiphanyCoordinatorAction::ReviewVerificationResult => {
ThreadEpiphanyCoordinatorAction::ReviewVerificationResult
}
CoreEpiphanyCoordinatorAction::ContinueImplementation => {
ThreadEpiphanyCoordinatorAction::ContinueImplementation
}
}
}
pub fn protocol_role_board_lanes(
role_board: &EpiphanyRoleBoardStatus,
) -> Vec<ThreadEpiphanyRoleLane> {
role_board
.roles
.iter()
.cloned()
.map(|lane| protocol_role_board_lane(lane, &role_board.source_jobs))
.collect()
}
fn protocol_role_board_lane(
lane: EpiphanyRoleBoardLane,
source_jobs: &[EpiphanyJobView],
) -> ThreadEpiphanyRoleLane {
ThreadEpiphanyRoleLane {
id: protocol_coordinator_role_id(lane.id),
title: lane.title,
owner_role: lane.owner_role,
status: protocol_coordinator_role_status(lane.status),
note: lane.note,
jobs: lane
.jobs
.iter()
.map(|job| protocol_role_board_job(job, source_jobs))
.collect(),
authority_scopes: lane.authority_scopes,
recommended_action: lane
.recommended_action
.map(protocol_coordinator_scene_action),
}
}
fn protocol_role_board_job(
job: &EpiphanyRoleBoardJob,
source_jobs: &[EpiphanyJobView],
) -> ThreadEpiphanyJob {
source_jobs
.iter()
.find(|source_job| source_job.id == job.id && source_job.owner_role == job.owner_role)
.cloned()
.map(|job| protocol_job_from_surface(job, None, None))
.unwrap_or_else(|| {
protocol_job_from_surface(
EpiphanyJobView {
id: job.id.clone(),
kind: CoreEpiphanyJobKind::Specialist,
scope: job.id.clone(),
owner_role: job.owner_role.clone(),
authority_scope: None,
runtime_job_id: None,
status: core_epiphany_job_status_from_role_board(job.status),
items_processed: None,
items_total: None,
progress_note: job.progress_note.clone(),
last_checkpoint_at_unix_seconds: None,
blocking_reason: job.blocking_reason.clone(),
active_thread_ids: Vec::new(),
linked_subgoal_ids: Vec::new(),
linked_graph_node_ids: Vec::new(),
},
None,
None,
)
})
}
fn core_epiphany_job_status_from_role_board(
status: EpiphanyRoleBoardJobStatus,
) -> CoreEpiphanyJobStatus {
match status {
EpiphanyRoleBoardJobStatus::Idle => CoreEpiphanyJobStatus::Idle,
EpiphanyRoleBoardJobStatus::Needed => CoreEpiphanyJobStatus::Needed,
EpiphanyRoleBoardJobStatus::Pending => CoreEpiphanyJobStatus::Pending,
EpiphanyRoleBoardJobStatus::Running => CoreEpiphanyJobStatus::Running,
EpiphanyRoleBoardJobStatus::Completed => CoreEpiphanyJobStatus::Completed,
EpiphanyRoleBoardJobStatus::Failed => CoreEpiphanyJobStatus::Failed,
EpiphanyRoleBoardJobStatus::Cancelled => CoreEpiphanyJobStatus::Cancelled,
EpiphanyRoleBoardJobStatus::Blocked => CoreEpiphanyJobStatus::Blocked,
EpiphanyRoleBoardJobStatus::Unavailable => CoreEpiphanyJobStatus::Unavailable,
}
}
fn protocol_coordinator_role_status(
status: CoreEpiphanyCoordinatorRoleStatus,
) -> ThreadEpiphanyRoleStatus {
match status {
CoreEpiphanyCoordinatorRoleStatus::Ready => ThreadEpiphanyRoleStatus::Ready,
CoreEpiphanyCoordinatorRoleStatus::Needed => ThreadEpiphanyRoleStatus::Needed,
CoreEpiphanyCoordinatorRoleStatus::Running => ThreadEpiphanyRoleStatus::Running,
CoreEpiphanyCoordinatorRoleStatus::Waiting => ThreadEpiphanyRoleStatus::Waiting,
CoreEpiphanyCoordinatorRoleStatus::Review => ThreadEpiphanyRoleStatus::Review,
CoreEpiphanyCoordinatorRoleStatus::Blocked => ThreadEpiphanyRoleStatus::Blocked,
CoreEpiphanyCoordinatorRoleStatus::Unavailable => ThreadEpiphanyRoleStatus::Unavailable,
CoreEpiphanyCoordinatorRoleStatus::Completed => ThreadEpiphanyRoleStatus::Completed,
}
}

View file

@ -0,0 +1,42 @@
use epiphany_core::EpiphanyGraphFreshness;
pub use epiphany_core::EpiphanyGraphFreshnessStatus;
use epiphany_core::EpiphanyInvalidationInput;
pub use epiphany_core::EpiphanyInvalidationStatus;
pub use epiphany_core::EpiphanyJobStatus;
pub use epiphany_core::EpiphanyJobView;
pub use epiphany_core::EpiphanyPressure;
pub use epiphany_core::EpiphanyPressureBasis;
pub use epiphany_core::EpiphanyPressureLevel;
pub use epiphany_core::EpiphanyPressureStatus;
pub use epiphany_core::EpiphanyReorientAction;
pub use epiphany_core::EpiphanyReorientCheckpointStatus;
pub use epiphany_core::EpiphanyReorientDecision;
pub use epiphany_core::EpiphanyReorientFreshnessStatus;
pub use epiphany_core::EpiphanyReorientPressureLevel;
pub use epiphany_core::EpiphanyReorientReason;
pub use epiphany_core::EpiphanyReorientStateStatus;
use epiphany_core::EpiphanyRetrievalFreshness;
pub use epiphany_core::EpiphanyRetrievalFreshnessStatus;
pub use epiphany_core::EpiphanyStateUpdatedField;
pub use epiphany_state_model::EpiphanyJobKind;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanySurfaceSource {
Stored,
Live,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyFreshnessSurface {
pub thread_id: String,
pub source: EpiphanySurfaceSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state_revision: Option<u64>,
pub retrieval: EpiphanyRetrievalFreshness,
pub graph: EpiphanyGraphFreshness,
pub watcher: EpiphanyInvalidationInput,
}

View file

@ -0,0 +1,20 @@
use std::error::Error;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EpiphanyBridgeError {
InvalidRequest(String),
Fatal(String),
}
pub type Result<T> = std::result::Result<T, EpiphanyBridgeError>;
impl fmt::Display for EpiphanyBridgeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidRequest(message) | Self::Fatal(message) => f.write_str(message),
}
}
}
impl Error for EpiphanyBridgeError {}

View file

@ -1,23 +1,18 @@
use codex_core::file_watcher::FileWatcher;
use codex_core::file_watcher::FileWatcherSubscriber;
use codex_core::file_watcher::ThrottledWatchReceiver;
use codex_core::file_watcher::WatchPath;
use codex_core::file_watcher::WatchRegistration;
use codex_utils_absolute_path::AbsolutePathBuf;
use notify::RecursiveMode;
use notify::Watcher;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use tokio::sync::Mutex as AsyncMutex;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tracing::warn;
use crate::reorient::EpiphanyFreshnessWatcherSnapshot;
const INVALIDATION_WATCH_DEBOUNCE: Duration = Duration::from_millis(200);
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EpiphanyInvalidationSnapshot {
pub available: bool,
@ -39,8 +34,6 @@ pub fn epiphany_freshness_watcher_snapshot(
#[derive(Clone)]
pub struct EpiphanyInvalidationManager {
available: bool,
file_watcher: Arc<FileWatcher>,
state: Arc<AsyncMutex<HashMap<String, WatchEntry>>>,
}
@ -48,8 +41,7 @@ struct WatchEntry {
workspace_root: AbsolutePathBuf,
latest: Arc<AsyncMutex<LatestInvalidation>>,
terminate_tx: oneshot::Sender<oneshot::Sender<()>>,
_subscriber: FileWatcherSubscriber,
_registration: WatchRegistration,
_watcher: notify::RecommendedWatcher,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
@ -60,28 +52,12 @@ struct LatestInvalidation {
impl EpiphanyInvalidationManager {
pub fn new() -> Self {
match FileWatcher::new() {
Ok(file_watcher) => Self {
available: true,
file_watcher: Arc::new(file_watcher),
state: Arc::new(AsyncMutex::new(HashMap::new())),
},
Err(err) => {
warn!("epiphany invalidation manager falling back to noop core watcher: {err}");
Self {
available: false,
file_watcher: Arc::new(FileWatcher::noop()),
state: Arc::new(AsyncMutex::new(HashMap::new())),
}
}
Self {
state: Arc::new(AsyncMutex::new(HashMap::new())),
}
}
pub async fn ensure_thread_watch(&self, thread_id: &str, workspace_root: &AbsolutePathBuf) {
if !self.available {
return;
}
let existing_entry = {
let mut state = self.state.lock().await;
match state.get(thread_id) {
@ -95,12 +71,30 @@ impl EpiphanyInvalidationManager {
stop_watch_entry(entry).await;
}
let (subscriber, rx) = self.file_watcher.add_subscriber();
let registration = subscriber.register_paths(vec![WatchPath {
path: workspace_root.to_path_buf(),
recursive: true,
}]);
let latest = Arc::new(AsyncMutex::new(LatestInvalidation::default()));
let (event_tx, mut event_rx) = mpsc::unbounded_channel::<Vec<PathBuf>>();
let mut watcher =
match notify::recommended_watcher(move |result: notify::Result<notify::Event>| {
match result {
Ok(event) => {
let _ = event_tx.send(event.paths);
}
Err(err) => warn!("epiphany invalidation watcher event failed: {err}"),
}
}) {
Ok(watcher) => watcher,
Err(err) => {
warn!("epiphany invalidation manager could not create watcher: {err}");
return;
}
};
if let Err(err) = watcher.watch(workspace_root.as_path(), RecursiveMode::Recursive) {
warn!(
"epiphany invalidation manager could not watch `{}`: {err}",
workspace_root.display()
);
return;
}
let (terminate_tx, terminate_rx) = oneshot::channel();
let workspace_root_path = workspace_root.to_path_buf();
@ -108,25 +102,23 @@ impl EpiphanyInvalidationManager {
workspace_root: workspace_root.clone(),
latest: Arc::clone(&latest),
terminate_tx,
_subscriber: subscriber,
_registration: registration,
_watcher: watcher,
};
self.state.lock().await.insert(thread_id.to_string(), entry);
tokio::spawn(async move {
let mut rx = ThrottledWatchReceiver::new(rx, INVALIDATION_WATCH_DEBOUNCE);
let mut terminate_rx = terminate_rx;
let mut done_tx = None;
loop {
let event = tokio::select! {
let paths = tokio::select! {
biased;
result = &mut terminate_rx => {
done_tx = result.ok();
break;
}
event = rx.recv() => match event {
Some(event) => event,
paths = event_rx.recv() => match paths {
Some(paths) => paths,
None => break,
},
};
@ -135,7 +127,7 @@ impl EpiphanyInvalidationManager {
let mut latest = latest.lock().await;
latest.observed_at_unix_seconds = Some(observed_at_unix_seconds);
latest.changed_paths =
normalize_changed_paths(event.paths, workspace_root_path.as_path());
normalize_changed_paths(paths, workspace_root_path.as_path());
}
if let Some(done_tx) = done_tx {
@ -145,15 +137,6 @@ impl EpiphanyInvalidationManager {
}
pub async fn snapshot(&self, thread_id: &str) -> EpiphanyInvalidationSnapshot {
if !self.available {
return EpiphanyInvalidationSnapshot {
available: false,
workspace_root: None,
observed_at_unix_seconds: None,
changed_paths: Vec::new(),
};
}
let (workspace_root, latest) = {
let state = self.state.lock().await;
match state.get(thread_id) {

View file

@ -1,77 +1,46 @@
use codex_app_server_protocol::ThreadEpiphanyJob;
use codex_app_server_protocol::ThreadEpiphanyJobKind;
use codex_app_server_protocol::ThreadEpiphanyJobStatus;
use codex_protocol::protocol::EpiphanyJobKind as CoreEpiphanyJobKind;
use codex_protocol::protocol::EpiphanyRetrievalState;
use codex_protocol::protocol::EpiphanyThreadState;
use epiphany_core::EpiphanyJobStatus as CoreEpiphanyJobStatus;
use epiphany_core::EpiphanyJobView;
use epiphany_core::EpiphanyJobsInput;
use epiphany_core::derive_jobs;
use epiphany_state_model::EpiphanyJobKind as CoreEpiphanyJobKind;
use epiphany_state_model::EpiphanyRetrievalState;
use epiphany_state_model::EpiphanyThreadState;
pub fn map_epiphany_jobs(
pub fn derive_epiphany_jobs(
state: Option<&EpiphanyThreadState>,
retrieval_override: Option<&EpiphanyRetrievalState>,
) -> Vec<ThreadEpiphanyJob> {
) -> Vec<EpiphanyJobView> {
derive_jobs(EpiphanyJobsInput {
state,
retrieval_override,
})
.into_iter()
.map(map_core_epiphany_job_view)
.collect()
}
fn map_core_epiphany_job_view(job: EpiphanyJobView) -> ThreadEpiphanyJob {
ThreadEpiphanyJob {
id: job.id,
kind: map_core_epiphany_job_kind(job.kind),
scope: job.scope,
owner_role: job.owner_role,
launcher_job_id: None,
authority_scope: job.authority_scope,
backend_job_id: job.runtime_job_id,
status: map_core_epiphany_job_status(job.status),
items_processed: job.items_processed,
items_total: job.items_total,
progress_note: job.progress_note,
last_checkpoint_at_unix_seconds: job.last_checkpoint_at_unix_seconds,
blocking_reason: job.blocking_reason,
active_thread_ids: job.active_thread_ids,
linked_subgoal_ids: job.linked_subgoal_ids,
linked_graph_node_ids: job.linked_graph_node_ids,
}
}
pub fn map_core_epiphany_job_kind(kind: CoreEpiphanyJobKind) -> ThreadEpiphanyJobKind {
match kind {
CoreEpiphanyJobKind::Indexing => ThreadEpiphanyJobKind::Indexing,
CoreEpiphanyJobKind::Remap => ThreadEpiphanyJobKind::Remap,
CoreEpiphanyJobKind::Verification => ThreadEpiphanyJobKind::Verification,
CoreEpiphanyJobKind::Specialist => ThreadEpiphanyJobKind::Specialist,
}
pub fn map_epiphany_jobs(
state: Option<&EpiphanyThreadState>,
retrieval_override: Option<&EpiphanyRetrievalState>,
) -> Vec<EpiphanyJobView> {
derive_epiphany_jobs(state, retrieval_override)
}
pub fn map_launched_epiphany_job(
state: &EpiphanyThreadState,
binding_id: &str,
launcher_job_id: &str,
backend_job_id: &str,
fallback_kind: CoreEpiphanyJobKind,
fallback_scope: &str,
) -> ThreadEpiphanyJob {
map_epiphany_jobs(Some(state), None)
) -> EpiphanyJobView {
derive_epiphany_jobs(Some(state), None)
.into_iter()
.find(|job| job.id == binding_id)
.unwrap_or_else(|| ThreadEpiphanyJob {
.unwrap_or_else(|| EpiphanyJobView {
id: binding_id.to_string(),
kind: map_core_epiphany_job_kind(fallback_kind),
kind: fallback_kind,
scope: fallback_scope.to_string(),
owner_role: "epiphany-harness".to_string(),
launcher_job_id: Some(launcher_job_id.to_string()),
authority_scope: None,
backend_job_id: Some(backend_job_id.to_string()),
status: ThreadEpiphanyJobStatus::Pending,
runtime_job_id: Some(backend_job_id.to_string()),
status: CoreEpiphanyJobStatus::Pending,
items_processed: None,
items_total: None,
progress_note: None,
@ -86,49 +55,34 @@ pub fn map_launched_epiphany_job(
pub fn map_interrupted_epiphany_job(
state: &EpiphanyThreadState,
binding_id: &str,
) -> ThreadEpiphanyJob {
map_epiphany_jobs(Some(state), None)
) -> EpiphanyJobView {
derive_epiphany_jobs(Some(state), None)
.into_iter()
.find(|job| job.id == binding_id)
.unwrap_or_else(|| {
epiphany_blocked_state_job(
binding_id,
ThreadEpiphanyJobKind::Specialist,
CoreEpiphanyJobKind::Specialist,
"role-scoped specialist work",
"Interrupted job binding was not reflected in Epiphany state.",
)
})
}
fn map_core_epiphany_job_status(status: CoreEpiphanyJobStatus) -> ThreadEpiphanyJobStatus {
match status {
CoreEpiphanyJobStatus::Idle => ThreadEpiphanyJobStatus::Idle,
CoreEpiphanyJobStatus::Needed => ThreadEpiphanyJobStatus::Needed,
CoreEpiphanyJobStatus::Pending => ThreadEpiphanyJobStatus::Pending,
CoreEpiphanyJobStatus::Running => ThreadEpiphanyJobStatus::Running,
CoreEpiphanyJobStatus::Completed => ThreadEpiphanyJobStatus::Completed,
CoreEpiphanyJobStatus::Failed => ThreadEpiphanyJobStatus::Failed,
CoreEpiphanyJobStatus::Cancelled => ThreadEpiphanyJobStatus::Cancelled,
CoreEpiphanyJobStatus::Blocked => ThreadEpiphanyJobStatus::Blocked,
CoreEpiphanyJobStatus::Unavailable => ThreadEpiphanyJobStatus::Unavailable,
}
}
pub fn epiphany_blocked_state_job(
id: &str,
kind: ThreadEpiphanyJobKind,
kind: CoreEpiphanyJobKind,
scope: &str,
blocking_reason: &str,
) -> ThreadEpiphanyJob {
ThreadEpiphanyJob {
) -> EpiphanyJobView {
EpiphanyJobView {
id: id.to_string(),
kind,
scope: scope.to_string(),
owner_role: "epiphany-harness".to_string(),
launcher_job_id: None,
authority_scope: None,
backend_job_id: None,
status: ThreadEpiphanyJobStatus::Blocked,
runtime_job_id: None,
status: CoreEpiphanyJobStatus::Blocked,
items_processed: None,
items_total: None,
progress_note: None,

View file

@ -1,25 +1,21 @@
use std::path::Path;
use std::sync::OnceLock;
use codex_app_server_protocol::ThreadEpiphanyGraphFreshnessStatus;
use codex_app_server_protocol::ThreadEpiphanyInvalidationStatus;
use codex_app_server_protocol::ThreadEpiphanyPressureLevel;
use codex_app_server_protocol::ThreadEpiphanyReorientAction;
use codex_app_server_protocol::ThreadEpiphanyReorientDecision;
use codex_app_server_protocol::ThreadEpiphanyReorientReason;
use codex_app_server_protocol::ThreadEpiphanyReorientWorkerLaunchDocument;
use codex_app_server_protocol::ThreadEpiphanyRetrievalFreshnessStatus;
use codex_app_server_protocol::ThreadEpiphanyRoleId;
use codex_app_server_protocol::ThreadEpiphanyRoleWorkerLaunchDocument;
use codex_app_server_protocol::ThreadEpiphanyWorkerLaunchDocument;
use codex_protocol::protocol::EpiphanyInvestigationCheckpoint;
use codex_protocol::protocol::EpiphanyInvestigationDisposition;
use codex_protocol::protocol::EpiphanyJobKind as CoreEpiphanyJobKind;
use codex_protocol::protocol::EpiphanyThreadState;
use epiphany_core::EpiphanyCoordinatorAction as CoreEpiphanyCoordinatorAction;
use epiphany_core::EpiphanyCoordinatorRoleResultStatus as CoreEpiphanyCoordinatorRoleResultStatus;
use epiphany_core::EpiphanyCrrcAction as CoreEpiphanyCrrcAction;
use epiphany_core::EpiphanyCrrcResultStatus as CoreEpiphanyCrrcResultStatus;
use epiphany_core::EpiphanyJobLaunchRequest;
use epiphany_core::EpiphanyReorientWorkerLaunchDocument;
use epiphany_core::EpiphanyPressureLevel as CoreEpiphanyPressureLevel;
use epiphany_core::EpiphanyReorientAction as CoreEpiphanyReorientAction;
use epiphany_core::EpiphanyReorientDecision as CoreEpiphanyReorientDecision;
use epiphany_core::EpiphanyReorientLaunchRequestInput;
use epiphany_core::EpiphanyRoleResultRoleId;
use epiphany_core::EpiphanyRoleWorkerLaunchDocument;
use epiphany_core::EpiphanyWorkerLaunchDocument;
use epiphany_core::build_reorient_job_launch_request;
use epiphany_state_model::EpiphanyInvestigationCheckpoint;
use epiphany_state_model::EpiphanyJobKind as CoreEpiphanyJobKind;
use epiphany_state_model::EpiphanyThreadState;
pub const EPIPHANY_IMAGINATION_ROLE_BINDING_ID: &str = "planning-synthesis-worker";
pub const EPIPHANY_IMAGINATION_OWNER_ROLE: &str = "epiphany-imagination";
@ -30,59 +26,61 @@ pub const EPIPHANY_VERIFICATION_OWNER_ROLE: &str = "epiphany-verifier";
pub const EPIPHANY_REORIENT_LAUNCH_BINDING_ID: &str = "reorient-worker";
pub const EPIPHANY_REORIENT_OWNER_ROLE: &str = "epiphany-reorient";
pub fn epiphany_role_binding_id(role_id: ThreadEpiphanyRoleId) -> Result<&'static str, String> {
pub fn epiphany_role_binding_id(role_id: EpiphanyRoleResultRoleId) -> Result<&'static str, String> {
match role_id {
ThreadEpiphanyRoleId::Imagination => Ok(EPIPHANY_IMAGINATION_ROLE_BINDING_ID),
ThreadEpiphanyRoleId::Modeling => Ok(EPIPHANY_MODELING_ROLE_BINDING_ID),
ThreadEpiphanyRoleId::Verification => Ok(EPIPHANY_VERIFICATION_ROLE_BINDING_ID),
ThreadEpiphanyRoleId::Implementation => Err(
EpiphanyRoleResultRoleId::Imagination => Ok(EPIPHANY_IMAGINATION_ROLE_BINDING_ID),
EpiphanyRoleResultRoleId::Modeling => Ok(EPIPHANY_MODELING_ROLE_BINDING_ID),
EpiphanyRoleResultRoleId::Verification => Ok(EPIPHANY_VERIFICATION_ROLE_BINDING_ID),
EpiphanyRoleResultRoleId::Implementation => Err(
"implementation is owned by the main coding agent; no role specialist launch template exists"
.to_string(),
),
ThreadEpiphanyRoleId::Reorientation => Err(
EpiphanyRoleResultRoleId::Reorientation => Err(
"reorientation uses thread/epiphany/reorientLaunch and thread/epiphany/reorientResult"
.to_string(),
),
}
}
pub fn epiphany_role_owner(role_id: ThreadEpiphanyRoleId) -> Result<&'static str, String> {
pub fn epiphany_role_owner(role_id: EpiphanyRoleResultRoleId) -> Result<&'static str, String> {
match role_id {
ThreadEpiphanyRoleId::Imagination => Ok(EPIPHANY_IMAGINATION_OWNER_ROLE),
ThreadEpiphanyRoleId::Modeling => Ok(EPIPHANY_MODELING_OWNER_ROLE),
ThreadEpiphanyRoleId::Verification => Ok(EPIPHANY_VERIFICATION_OWNER_ROLE),
ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => {
EpiphanyRoleResultRoleId::Imagination => Ok(EPIPHANY_IMAGINATION_OWNER_ROLE),
EpiphanyRoleResultRoleId::Modeling => Ok(EPIPHANY_MODELING_OWNER_ROLE),
EpiphanyRoleResultRoleId::Verification => Ok(EPIPHANY_VERIFICATION_OWNER_ROLE),
EpiphanyRoleResultRoleId::Implementation | EpiphanyRoleResultRoleId::Reorientation => {
Err(epiphany_role_binding_id(role_id).unwrap_err())
}
}
}
pub fn epiphany_role_label(role_id: ThreadEpiphanyRoleId) -> &'static str {
pub fn epiphany_role_label(role_id: EpiphanyRoleResultRoleId) -> &'static str {
match role_id {
ThreadEpiphanyRoleId::Implementation => "implementation",
ThreadEpiphanyRoleId::Imagination => "imagination",
ThreadEpiphanyRoleId::Modeling => "modeling",
ThreadEpiphanyRoleId::Verification => "verification",
ThreadEpiphanyRoleId::Reorientation => "reorientation",
EpiphanyRoleResultRoleId::Implementation => "implementation",
EpiphanyRoleResultRoleId::Imagination => "imagination",
EpiphanyRoleResultRoleId::Modeling => "modeling",
EpiphanyRoleResultRoleId::Verification => "verification",
EpiphanyRoleResultRoleId::Reorientation => "reorientation",
}
}
pub fn epiphany_role_launch_output_schema(role_id: ThreadEpiphanyRoleId) -> serde_json::Value {
pub fn epiphany_role_launch_output_schema(role_id: EpiphanyRoleResultRoleId) -> serde_json::Value {
let verdict_enum = match role_id {
ThreadEpiphanyRoleId::Imagination => {
EpiphanyRoleResultRoleId::Imagination => {
vec!["draft-ready", "planning-update-needed", "blocked"]
}
ThreadEpiphanyRoleId::Modeling => {
EpiphanyRoleResultRoleId::Modeling => {
vec![
"checkpoint-ready",
"checkpoint-update-needed",
"regather-needed",
]
}
ThreadEpiphanyRoleId::Verification => {
EpiphanyRoleResultRoleId::Verification => {
vec!["pass", "needs-review", "needs-evidence", "fail"]
}
ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => vec![],
EpiphanyRoleResultRoleId::Implementation | EpiphanyRoleResultRoleId::Reorientation => {
vec![]
}
};
let mut properties = serde_json::json!({
"roleId": {
@ -194,7 +192,7 @@ pub fn epiphany_role_launch_output_schema(role_id: ThreadEpiphanyRoleId) -> serd
"nextSafeMove",
"filesInspected",
];
if role_id == ThreadEpiphanyRoleId::Imagination {
if role_id == EpiphanyRoleResultRoleId::Imagination {
if let Some(map) = properties.as_object_mut() {
map.insert(
"statePatch".to_string(),
@ -231,7 +229,7 @@ pub fn epiphany_role_launch_output_schema(role_id: ThreadEpiphanyRoleId) -> serd
);
}
required.push("statePatch");
} else if role_id == ThreadEpiphanyRoleId::Modeling {
} else if role_id == EpiphanyRoleResultRoleId::Modeling {
if let Some(map) = properties.as_object_mut() {
map.insert(
"statePatch".to_string(),
@ -390,7 +388,7 @@ pub fn epiphany_agent_prompt_with_memory(body: &str) -> String {
pub fn build_epiphany_role_launch_request(
thread_id: &str,
role_id: ThreadEpiphanyRoleId,
role_id: EpiphanyRoleResultRoleId,
expected_revision: Option<u64>,
max_runtime_seconds: Option<u64>,
state: &EpiphanyThreadState,
@ -400,22 +398,22 @@ pub fn build_epiphany_role_launch_request(
let linked_subgoal_ids = epiphany_active_subgoal_ids(Some(state));
let linked_graph_node_ids = epiphany_active_graph_node_ids(Some(state));
let (scope, authority_scope, instruction) = match role_id {
ThreadEpiphanyRoleId::Imagination => (
EpiphanyRoleResultRoleId::Imagination => (
"role-scoped planning synthesis",
"epiphany.role.imagination",
build_epiphany_role_launch_instruction(role_id),
),
ThreadEpiphanyRoleId::Modeling => (
EpiphanyRoleResultRoleId::Modeling => (
"role-scoped modeling/checkpoint maintenance",
"epiphany.role.modeling",
build_epiphany_role_launch_instruction(role_id),
),
ThreadEpiphanyRoleId::Verification => (
EpiphanyRoleResultRoleId::Verification => (
"role-scoped verification/review",
"epiphany.role.verification",
build_epiphany_role_launch_instruction(role_id),
),
ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => {
EpiphanyRoleResultRoleId::Implementation | EpiphanyRoleResultRoleId::Reorientation => {
return Err(epiphany_role_binding_id(role_id).unwrap_err());
}
};
@ -461,13 +459,13 @@ pub fn build_epiphany_role_launch_request(
})
}
fn build_epiphany_role_launch_instruction(role_id: ThreadEpiphanyRoleId) -> String {
fn build_epiphany_role_launch_instruction(role_id: EpiphanyRoleResultRoleId) -> String {
let prompts = &epiphany_specialist_prompt_config().roles;
let body = match role_id {
ThreadEpiphanyRoleId::Imagination => prompts.imagination.as_str(),
ThreadEpiphanyRoleId::Modeling => prompts.modeling.as_str(),
ThreadEpiphanyRoleId::Verification => prompts.verification.as_str(),
ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => {
EpiphanyRoleResultRoleId::Imagination => prompts.imagination.as_str(),
EpiphanyRoleResultRoleId::Modeling => prompts.modeling.as_str(),
EpiphanyRoleResultRoleId::Verification => prompts.verification.as_str(),
EpiphanyRoleResultRoleId::Implementation | EpiphanyRoleResultRoleId::Reorientation => {
"Unsupported Epiphany role specialist template."
}
};
@ -480,85 +478,54 @@ pub fn build_epiphany_reorient_launch_request(
max_runtime_seconds: Option<u64>,
state: &EpiphanyThreadState,
checkpoint: &EpiphanyInvestigationCheckpoint,
decision: &ThreadEpiphanyReorientDecision,
decision: &CoreEpiphanyReorientDecision,
) -> EpiphanyJobLaunchRequest {
let linked_subgoal_ids = epiphany_active_subgoal_ids(Some(state));
let linked_graph_node_ids = unique_strings(
epiphany_active_graph_node_ids(Some(state))
.into_iter()
.chain(decision.active_frontier_node_ids.iter().cloned()),
);
let checkpoint_next_action = checkpoint
.next_action
.clone()
.unwrap_or_else(|| decision.next_action.clone());
let authority_scope = match decision.action {
ThreadEpiphanyReorientAction::Resume => "epiphany.reorient.resume",
ThreadEpiphanyReorientAction::Regather => "epiphany.reorient.regather",
};
let scope = match decision.action {
ThreadEpiphanyReorientAction::Resume => "reorient-guided checkpoint resume",
ThreadEpiphanyReorientAction::Regather => "reorient-guided checkpoint regather",
};
let instruction = build_epiphany_reorient_launch_instruction(decision.action);
let launch_document =
EpiphanyWorkerLaunchDocument::Reorient(EpiphanyReorientWorkerLaunchDocument {
thread_id: thread_id.to_string(),
mode: reorient_action_label(decision.action).to_string(),
checkpoint_id: checkpoint.checkpoint_id.clone(),
checkpoint_kind: checkpoint.kind.clone(),
checkpoint_disposition: investigation_disposition_label(checkpoint.disposition)
.to_string(),
checkpoint_focus: Some(checkpoint.focus.clone()),
checkpoint_summary: checkpoint.summary.clone(),
checkpoint_next_action,
checkpoint_open_questions: checkpoint.open_questions.clone(),
checkpoint_evidence_ids: checkpoint.evidence_ids.clone(),
checkpoint_code_refs: checkpoint.code_refs.clone(),
decision_reasons: decision
.reasons
.iter()
.map(|reason| reorient_reason_label(*reason).to_string())
.collect(),
decision_note: decision.note.clone(),
pressure_level: pressure_level_label(decision.pressure_level).to_string(),
retrieval_status: retrieval_freshness_status_label(decision.retrieval_status)
.to_string(),
graph_status: graph_freshness_status_label(decision.graph_status).to_string(),
watcher_status: invalidation_status_label(decision.watcher_status).to_string(),
checkpoint_dirty_paths: decision
.checkpoint_dirty_paths
.iter()
.map(path_to_display_string)
.collect(),
checkpoint_changed_paths: decision
.checkpoint_changed_paths
.iter()
.map(path_to_display_string)
.collect(),
scratch: state.scratch.clone(),
graphs: Some(state.graphs.clone()),
recent_evidence: state.recent_evidence.iter().take(8).cloned().collect(),
recent_observations: state.observations.iter().take(8).cloned().collect(),
active_frontier_node_ids: decision.active_frontier_node_ids.clone(),
linked_subgoal_ids: linked_subgoal_ids.clone(),
linked_graph_node_ids: linked_graph_node_ids.clone(),
});
let output_contract_id = launch_document.output_contract_id().to_string();
build_reorient_job_launch_request(EpiphanyReorientLaunchRequestInput {
thread_id,
expected_revision,
max_runtime_seconds,
binding_id: EPIPHANY_REORIENT_LAUNCH_BINDING_ID,
owner_role: EPIPHANY_REORIENT_OWNER_ROLE,
instruction,
state,
checkpoint,
decision,
})
}
pub fn build_epiphany_reorient_launch_instruction(action: CoreEpiphanyReorientAction) -> String {
let prompts = &epiphany_specialist_prompt_config().reorientation;
let body = match action {
CoreEpiphanyReorientAction::Resume => prompts.resume.as_str(),
CoreEpiphanyReorientAction::Regather => prompts.regather.as_str(),
};
epiphany_agent_prompt_with_memory(body)
}
pub fn build_epiphany_job_launch_request(
expected_revision: Option<u64>,
binding_id: String,
kind: CoreEpiphanyJobKind,
scope: String,
owner_role: String,
authority_scope: String,
linked_subgoal_ids: Vec<String>,
linked_graph_node_ids: Vec<String>,
instruction: String,
launch_document: EpiphanyWorkerLaunchDocument,
output_contract_id: String,
max_runtime_seconds: Option<u64>,
) -> EpiphanyJobLaunchRequest {
EpiphanyJobLaunchRequest {
expected_revision,
binding_id: EPIPHANY_REORIENT_LAUNCH_BINDING_ID.to_string(),
kind: CoreEpiphanyJobKind::Specialist,
scope: scope.to_string(),
owner_role: EPIPHANY_REORIENT_OWNER_ROLE.to_string(),
authority_scope: authority_scope.to_string(),
linked_subgoal_ids: epiphany_active_subgoal_ids(Some(state)),
linked_graph_node_ids: unique_strings(
epiphany_active_graph_node_ids(Some(state))
.into_iter()
.chain(decision.active_frontier_node_ids.iter().cloned()),
),
binding_id,
kind,
scope,
owner_role,
authority_scope,
linked_subgoal_ids,
linked_graph_node_ids,
instruction,
launch_document,
output_contract_id,
@ -566,94 +533,13 @@ pub fn build_epiphany_reorient_launch_request(
}
}
pub fn build_epiphany_reorient_launch_instruction(action: ThreadEpiphanyReorientAction) -> String {
let prompts = &epiphany_specialist_prompt_config().reorientation;
let body = match action {
ThreadEpiphanyReorientAction::Resume => prompts.resume.as_str(),
ThreadEpiphanyReorientAction::Regather => prompts.regather.as_str(),
};
epiphany_agent_prompt_with_memory(body)
}
pub fn map_core_worker_launch_document(
document: ThreadEpiphanyWorkerLaunchDocument,
) -> EpiphanyWorkerLaunchDocument {
match document {
ThreadEpiphanyWorkerLaunchDocument::Role(document) => {
EpiphanyWorkerLaunchDocument::Role(map_core_role_worker_launch_document(document))
}
ThreadEpiphanyWorkerLaunchDocument::Reorient(document) => {
EpiphanyWorkerLaunchDocument::Reorient(map_core_reorient_worker_launch_document(
document,
))
}
}
}
fn map_core_role_worker_launch_document(
document: ThreadEpiphanyRoleWorkerLaunchDocument,
) -> EpiphanyRoleWorkerLaunchDocument {
EpiphanyRoleWorkerLaunchDocument {
thread_id: document.thread_id,
role_id: document.role_id,
state_revision: document.state_revision,
objective: document.objective,
active_subgoal_id: document.active_subgoal_id,
active_subgoals: document.active_subgoals,
active_graph_node_ids: document.active_graph_node_ids,
investigation_checkpoint: document.investigation_checkpoint,
scratch: document.scratch,
invariants: document.invariants,
graphs: document.graphs,
recent_evidence: document.recent_evidence,
recent_observations: document.recent_observations,
graph_frontier: document.graph_frontier,
graph_checkpoint: document.graph_checkpoint,
planning: document.planning,
churn: document.churn,
}
}
fn map_core_reorient_worker_launch_document(
document: ThreadEpiphanyReorientWorkerLaunchDocument,
) -> EpiphanyReorientWorkerLaunchDocument {
EpiphanyReorientWorkerLaunchDocument {
thread_id: document.thread_id,
mode: document.mode,
checkpoint_id: document.checkpoint_id,
checkpoint_kind: document.checkpoint_kind,
checkpoint_disposition: document.checkpoint_disposition,
checkpoint_focus: document.checkpoint_focus,
checkpoint_summary: document.checkpoint_summary,
checkpoint_next_action: document.checkpoint_next_action,
checkpoint_open_questions: document.checkpoint_open_questions,
checkpoint_evidence_ids: document.checkpoint_evidence_ids,
checkpoint_code_refs: document.checkpoint_code_refs,
decision_reasons: document.decision_reasons,
decision_note: document.decision_note,
pressure_level: document.pressure_level,
retrieval_status: document.retrieval_status,
graph_status: document.graph_status,
watcher_status: document.watcher_status,
checkpoint_dirty_paths: document.checkpoint_dirty_paths,
checkpoint_changed_paths: document.checkpoint_changed_paths,
scratch: document.scratch,
graphs: document.graphs,
recent_evidence: document.recent_evidence,
recent_observations: document.recent_observations,
active_frontier_node_ids: document.active_frontier_node_ids,
linked_subgoal_ids: document.linked_subgoal_ids,
linked_graph_node_ids: document.linked_graph_node_ids,
}
}
pub fn render_epiphany_coordinator_note(
crrc_action: codex_app_server_protocol::ThreadEpiphanyCrrcAction,
pressure_level: ThreadEpiphanyPressureLevel,
modeling_result_status: codex_app_server_protocol::ThreadEpiphanyRoleResultStatus,
verification_result_status: codex_app_server_protocol::ThreadEpiphanyRoleResultStatus,
reorient_result_status: codex_app_server_protocol::ThreadEpiphanyReorientResultStatus,
coordinator_action: codex_app_server_protocol::ThreadEpiphanyCoordinatorAction,
crrc_action: CoreEpiphanyCrrcAction,
pressure_level: CoreEpiphanyPressureLevel,
modeling_result_status: CoreEpiphanyCoordinatorRoleResultStatus,
verification_result_status: CoreEpiphanyCoordinatorRoleResultStatus,
reorient_result_status: CoreEpiphanyCrrcResultStatus,
coordinator_action: CoreEpiphanyCoordinatorAction,
) -> String {
let template = epiphany_agent_prompt_with_memory(
&epiphany_specialist_prompt_config()
@ -706,74 +592,3 @@ fn epiphany_active_graph_node_ids(state: Option<&EpiphanyThreadState>) -> Vec<St
.map(|frontier| frontier.active_node_ids.clone())
.unwrap_or_default()
}
fn path_to_display_string(path: impl AsRef<Path>) -> String {
path.as_ref().to_string_lossy().to_string()
}
fn reorient_action_label(action: ThreadEpiphanyReorientAction) -> &'static str {
match action {
ThreadEpiphanyReorientAction::Resume => "resume",
ThreadEpiphanyReorientAction::Regather => "regather",
}
}
fn investigation_disposition_label(disposition: EpiphanyInvestigationDisposition) -> &'static str {
match disposition {
EpiphanyInvestigationDisposition::ResumeReady => "resume_ready",
EpiphanyInvestigationDisposition::RegatherRequired => "regather_required",
}
}
fn reorient_reason_label(reason: ThreadEpiphanyReorientReason) -> &'static str {
match reason {
ThreadEpiphanyReorientReason::MissingState => "missingState",
ThreadEpiphanyReorientReason::MissingCheckpoint => "missingCheckpoint",
ThreadEpiphanyReorientReason::CheckpointRequestedRegather => "checkpointRequestedRegather",
ThreadEpiphanyReorientReason::CheckpointPathsDirty => "checkpointPathsDirty",
ThreadEpiphanyReorientReason::CheckpointPathsChanged => "checkpointPathsChanged",
ThreadEpiphanyReorientReason::FrontierChanged => "frontierChanged",
ThreadEpiphanyReorientReason::UnanchoredCheckpointWhileStateStale => {
"unanchoredCheckpointWhileStateStale"
}
ThreadEpiphanyReorientReason::CheckpointReady => "checkpointReady",
}
}
pub fn pressure_level_label(level: ThreadEpiphanyPressureLevel) -> &'static str {
match level {
ThreadEpiphanyPressureLevel::Unknown => "unknown",
ThreadEpiphanyPressureLevel::Low => "low",
ThreadEpiphanyPressureLevel::Elevated => "elevated",
ThreadEpiphanyPressureLevel::High => "high",
ThreadEpiphanyPressureLevel::Critical => "critical",
}
}
fn retrieval_freshness_status_label(
status: ThreadEpiphanyRetrievalFreshnessStatus,
) -> &'static str {
match status {
ThreadEpiphanyRetrievalFreshnessStatus::Missing => "missing",
ThreadEpiphanyRetrievalFreshnessStatus::Ready => "ready",
ThreadEpiphanyRetrievalFreshnessStatus::Stale => "stale",
ThreadEpiphanyRetrievalFreshnessStatus::Indexing => "indexing",
ThreadEpiphanyRetrievalFreshnessStatus::Unavailable => "unavailable",
}
}
fn graph_freshness_status_label(status: ThreadEpiphanyGraphFreshnessStatus) -> &'static str {
match status {
ThreadEpiphanyGraphFreshnessStatus::Missing => "missing",
ThreadEpiphanyGraphFreshnessStatus::Ready => "ready",
ThreadEpiphanyGraphFreshnessStatus::Stale => "stale",
}
}
fn invalidation_status_label(status: ThreadEpiphanyInvalidationStatus) -> &'static str {
match status {
ThreadEpiphanyInvalidationStatus::Unavailable => "unavailable",
ThreadEpiphanyInvalidationStatus::Clean => "clean",
ThreadEpiphanyInvalidationStatus::Changed => "changed",
}
}

View file

@ -1,20 +1,40 @@
//! Compatibility bridge between Epiphany's typed CultNet/CultCache-shaped
//! contracts and the vendored Codex JSON-RPC/app-server shell.
//!
//! This crate is a bridge, not an authority organ. Its job is to translate
//! Codex `ThreadEpiphany*` DTOs into Epiphany core documents, project typed
//! Epiphany surfaces back into legacy Codex responses, and call the narrow
//! host trait needed while Codex still owns thread persistence. Verdicts,
//! launch policy, reorientation policy, acceptance rules, runtime-spine
//! lifecycle semantics, and durable state invariants belong in `epiphany-core`
//! or `epiphany-state-model`.
//!
//! Host facts and side effects that still pass through this crate are
//! quarantine scaffolding for the current Codex shell. Do not let them become
//! a second policy throne. The long cut is to replace these adapters with
//! native CultNet intents and receipts, leaving Codex only as the OpenAI
//! subscription auth/model transport reliquary.
pub mod checkpoint;
pub mod context;
pub mod context_protocol;
pub mod coordinator;
pub mod coordinator_protocol;
pub mod cultnet;
pub mod error;
pub mod invalidation;
pub mod jobs;
pub mod launch;
pub mod mutation;
pub mod mutation_service;
pub mod pressure;
pub mod protocol_edge;
pub mod reorient;
pub mod results;
pub mod retrieve;
pub mod retrieve_protocol;
pub mod runtime_results;
pub mod scene;
pub mod scene_protocol;
pub mod state;
pub mod token_usage;
pub mod view;
pub use epiphany_openai_adapter as openai_adapter;
pub use epiphany_openai_codex_spine as openai_codex_spine;
pub mod view_protocol;

View file

@ -1,83 +1,52 @@
use std::path::PathBuf;
use codex_app_server_protocol::ThreadEpiphanyReorientFinding;
use codex_app_server_protocol::ThreadEpiphanyRoleFinding;
use codex_app_server_protocol::ThreadEpiphanyRoleId;
use codex_app_server_protocol::ThreadEpiphanyStateUpdatedField;
use codex_app_server_protocol::ThreadEpiphanyStateUpdatedNotification;
use codex_app_server_protocol::ThreadEpiphanyStateUpdatedSource;
use codex_app_server_protocol::ThreadEpiphanyUpdatePatch;
use codex_protocol::protocol::EpiphanyCodeRef;
use codex_protocol::protocol::EpiphanyInvestigationCheckpoint;
use codex_protocol::protocol::EpiphanyInvestigationDisposition;
use codex_protocol::protocol::EpiphanyScratchPad;
use epiphany_core::EpiphanyReorientAcceptanceFinding;
use epiphany_core::EpiphanyReorientFindingInterpretation as CoreEpiphanyReorientFinding;
use epiphany_core::EpiphanyRoleAcceptanceFinding;
use epiphany_core::EpiphanyRoleFindingInterpretation as CoreEpiphanyRoleFinding;
use epiphany_core::EpiphanyRoleResultRoleId;
use epiphany_core::EpiphanyRoleStatePatchDocument;
use epiphany_core::EpiphanyStateUpdate;
use epiphany_core::EpiphanyStateUpdatedField;
use epiphany_core::MindGatewayReview;
use epiphany_core::build_reorient_acceptance_bundle;
use epiphany_core::build_role_acceptance_bundle;
use epiphany_core::imagination_role_state_patch_policy_errors;
use epiphany_core::mind_review_allows_state;
use epiphany_core::mind_review_reorient_acceptance;
use epiphany_core::mind_review_role_acceptance;
use epiphany_core::modeling_role_state_patch_policy_errors;
use crate::results::map_core_role_result_role_id;
use crate::runtime_results::reorient_finding_runtime_job_id;
use crate::runtime_results::reorient_finding_runtime_result_id;
use crate::runtime_results::role_finding_runtime_job_id;
use crate::runtime_results::role_finding_runtime_result_id;
use epiphany_state_model::EpiphanyInvestigationCheckpoint;
pub struct RoleAcceptanceUpdate {
pub patch: ThreadEpiphanyUpdatePatch,
pub changed_fields: Vec<ThreadEpiphanyStateUpdatedField>,
pub state_update: EpiphanyStateUpdate,
pub applied_patch: EpiphanyRoleStatePatchDocument,
pub changed_fields: Vec<EpiphanyStateUpdatedField>,
pub accepted_receipt_id: String,
pub accepted_observation_id: String,
pub accepted_evidence_id: String,
pub mind_review: MindGatewayReview,
}
pub struct ReorientAcceptanceUpdate {
pub state_update: EpiphanyStateUpdate,
pub changed_fields: Vec<ThreadEpiphanyStateUpdatedField>,
pub changed_fields: Vec<EpiphanyStateUpdatedField>,
pub accepted_receipt_id: String,
pub accepted_observation_id: String,
pub accepted_evidence_id: String,
pub mind_review: MindGatewayReview,
}
pub fn parse_role_finding_state_patch(
finding: &ThreadEpiphanyRoleFinding,
) -> Result<ThreadEpiphanyUpdatePatch, String> {
pub fn parse_core_role_finding_state_patch(
finding: &CoreEpiphanyRoleFinding,
) -> Result<EpiphanyRoleStatePatchDocument, String> {
finding
.state_patch
.clone()
.ok_or_else(|| "completed role finding did not include a reviewable statePatch".to_string())
}
pub fn core_state_patch_from_protocol(
patch: &ThreadEpiphanyUpdatePatch,
) -> EpiphanyRoleStatePatchDocument {
EpiphanyRoleStatePatchDocument {
objective: patch.objective.clone(),
active_subgoal_id: patch.active_subgoal_id.clone(),
subgoals: patch.subgoals.clone(),
invariants: patch.invariants.clone(),
graphs: patch.graphs.clone(),
graph_frontier: patch.graph_frontier.clone(),
graph_checkpoint: patch.graph_checkpoint.clone(),
scratch: patch.scratch.clone(),
investigation_checkpoint: patch.investigation_checkpoint.clone(),
job_bindings: patch.job_bindings.clone(),
acceptance_receipts: patch.acceptance_receipts.clone(),
runtime_links: patch.runtime_links.clone(),
observations: patch.observations.clone(),
evidence: patch.evidence.clone(),
churn: patch.churn.clone(),
mode: patch.mode.clone(),
planning: patch.planning.clone(),
}
}
pub fn state_update_from_thread_patch(
pub fn state_update_from_core_patch(
expected_revision: Option<u64>,
patch: ThreadEpiphanyUpdatePatch,
patch: EpiphanyRoleStatePatchDocument,
) -> EpiphanyStateUpdate {
EpiphanyStateUpdate {
expected_revision,
@ -101,62 +70,19 @@ pub fn state_update_from_thread_patch(
}
}
pub fn epiphany_state_updated_notification(
thread_id: String,
source: ThreadEpiphanyStateUpdatedSource,
revision: u64,
changed_fields: Vec<ThreadEpiphanyStateUpdatedField>,
epiphany_state: codex_protocol::protocol::EpiphanyThreadState,
) -> ThreadEpiphanyStateUpdatedNotification {
ThreadEpiphanyStateUpdatedNotification {
thread_id,
source,
revision,
changed_fields,
epiphany_state,
}
}
pub fn imagination_role_accept_patch_errors(patch: &ThreadEpiphanyUpdatePatch) -> Vec<String> {
imagination_role_state_patch_policy_errors(&core_state_patch_from_protocol(patch))
}
pub fn modeling_role_accept_patch_errors(patch: &ThreadEpiphanyUpdatePatch) -> Vec<String> {
modeling_role_state_patch_policy_errors(&core_state_patch_from_protocol(patch))
}
pub fn epiphany_modeling_finding_has_reviewable_state_patch(
finding: &ThreadEpiphanyRoleFinding,
) -> bool {
finding.role_id == ThreadEpiphanyRoleId::Modeling
&& finding
.state_patch
.as_ref()
.is_some_and(|patch| modeling_role_accept_patch_errors(patch).is_empty())
}
pub fn epiphany_imagination_finding_has_reviewable_state_patch(
finding: &ThreadEpiphanyRoleFinding,
) -> bool {
finding.role_id == ThreadEpiphanyRoleId::Imagination
&& finding
.state_patch
.as_ref()
.is_some_and(|patch| imagination_role_accept_patch_errors(patch).is_empty())
}
pub fn build_role_acceptance_update(
role_id: ThreadEpiphanyRoleId,
expected_revision: Option<u64>,
role_id: EpiphanyRoleResultRoleId,
binding_id: &str,
finding: &ThreadEpiphanyRoleFinding,
finding: &CoreEpiphanyRoleFinding,
accepted_evidence_id: String,
accepted_observation_id: String,
accepted_at: String,
) -> Result<RoleAcceptanceUpdate, String> {
let mut patch = match role_id {
ThreadEpiphanyRoleId::Imagination => {
let patch = parse_role_finding_state_patch(finding)?;
let patch_errors = imagination_role_accept_patch_errors(&patch);
let mut core_patch = match role_id {
EpiphanyRoleResultRoleId::Imagination => {
let patch = parse_core_role_finding_state_patch(finding)?;
let patch_errors = imagination_role_state_patch_policy_errors(&patch);
if !patch_errors.is_empty() {
return Err(format!(
"imagination role state patch is not acceptable: {}",
@ -165,9 +91,9 @@ pub fn build_role_acceptance_update(
}
patch
}
ThreadEpiphanyRoleId::Modeling => {
let patch = parse_role_finding_state_patch(finding)?;
let patch_errors = modeling_role_accept_patch_errors(&patch);
EpiphanyRoleResultRoleId::Modeling => {
let patch = parse_core_role_finding_state_patch(finding)?;
let patch_errors = modeling_role_state_patch_policy_errors(&patch);
if !patch_errors.is_empty() {
return Err(format!(
"modeling role state patch is not acceptable: {}",
@ -176,8 +102,8 @@ pub fn build_role_acceptance_update(
}
patch
}
ThreadEpiphanyRoleId::Verification => ThreadEpiphanyUpdatePatch::default(),
ThreadEpiphanyRoleId::Implementation | ThreadEpiphanyRoleId::Reorientation => {
EpiphanyRoleResultRoleId::Verification => EpiphanyRoleStatePatchDocument::default(),
EpiphanyRoleResultRoleId::Implementation | EpiphanyRoleResultRoleId::Reorientation => {
return Err(format!(
"role {:?} cannot be accepted through roleAccept",
role_id
@ -185,20 +111,22 @@ pub fn build_role_acceptance_update(
}
};
let projected_fields = epiphany_update_patch_changed_fields(&patch)
let projected_fields = epiphany_update_patch_changed_fields(&core_patch)
.into_iter()
.map(|field| format!("{field:?}"))
.collect();
let mind_review = mind_review_role_acceptance(binding_id, role_id, finding, &core_patch);
mind_review_allows_state(&mind_review)?;
let acceptance_bundle = build_role_acceptance_bundle(
binding_id,
EpiphanyRoleAcceptanceFinding {
role_id: map_core_role_result_role_id(role_id),
role_id,
verdict: finding.verdict.clone(),
summary: finding.summary.clone(),
next_safe_move: finding.next_safe_move.clone(),
files_inspected: finding.files_inspected.clone(),
runtime_result_id: role_finding_runtime_result_id(finding),
runtime_job_id: role_finding_runtime_job_id(finding),
runtime_result_id: finding.runtime_result_id.clone(),
runtime_job_id: finding.runtime_job_id.clone(),
projected_fields,
},
accepted_evidence_id,
@ -208,24 +136,30 @@ pub fn build_role_acceptance_update(
let accepted_receipt_id = acceptance_bundle.accepted_receipt_id.clone();
let accepted_observation_id = acceptance_bundle.accepted_observation_id.clone();
let accepted_evidence_id = acceptance_bundle.accepted_evidence_id.clone();
patch.evidence.push(acceptance_bundle.evidence);
patch.observations.push(acceptance_bundle.observation);
patch.acceptance_receipts.push(acceptance_bundle.receipt);
let changed_fields = epiphany_update_patch_changed_fields(&patch);
core_patch.evidence.push(acceptance_bundle.evidence);
core_patch.observations.push(acceptance_bundle.observation);
core_patch
.acceptance_receipts
.push(acceptance_bundle.receipt);
let applied_patch = core_patch.clone();
let changed_fields = epiphany_update_patch_changed_fields(&applied_patch);
let state_update = state_update_from_core_patch(expected_revision, core_patch);
Ok(RoleAcceptanceUpdate {
patch,
state_update,
applied_patch,
changed_fields,
accepted_receipt_id,
accepted_observation_id,
accepted_evidence_id,
mind_review,
})
}
pub fn build_reorient_acceptance_update(
expected_revision: Option<u64>,
binding_id: &str,
finding: &ThreadEpiphanyReorientFinding,
finding: &CoreEpiphanyReorientFinding,
accepted_evidence_id: String,
accepted_observation_id: String,
accepted_at: String,
@ -233,17 +167,25 @@ pub fn build_reorient_acceptance_update(
update_investigation_checkpoint: bool,
checkpoint: Option<EpiphanyInvestigationCheckpoint>,
) -> Result<ReorientAcceptanceUpdate, String> {
let mind_finding = EpiphanyReorientAcceptanceFinding {
mode: finding.mode.clone(),
summary: finding.summary.clone(),
next_safe_move: finding.next_safe_move.clone(),
checkpoint_still_valid: finding.checkpoint_still_valid,
files_inspected: finding.files_inspected.clone(),
runtime_result_id: finding.runtime_result_id.clone(),
runtime_job_id: finding.runtime_job_id.clone(),
};
let mind_review = mind_review_reorient_acceptance(
binding_id,
&mind_finding,
update_scratch,
update_investigation_checkpoint,
);
mind_review_allows_state(&mind_review)?;
let acceptance_bundle = build_reorient_acceptance_bundle(
binding_id,
EpiphanyReorientAcceptanceFinding {
mode: finding.mode.clone(),
summary: finding.summary.clone(),
next_safe_move: finding.next_safe_move.clone(),
checkpoint_still_valid: finding.checkpoint_still_valid,
files_inspected: finding.files_inspected.clone(),
runtime_result_id: reorient_finding_runtime_result_id(finding),
runtime_job_id: reorient_finding_runtime_job_id(finding),
},
mind_finding,
accepted_evidence_id,
accepted_observation_id,
accepted_at,
@ -259,15 +201,15 @@ pub fn build_reorient_acceptance_update(
let investigation_checkpoint = acceptance_bundle.investigation_checkpoint;
let mut changed_fields = vec![
ThreadEpiphanyStateUpdatedField::AcceptanceReceipts,
ThreadEpiphanyStateUpdatedField::Observations,
ThreadEpiphanyStateUpdatedField::Evidence,
EpiphanyStateUpdatedField::AcceptanceReceipts,
EpiphanyStateUpdatedField::Observations,
EpiphanyStateUpdatedField::Evidence,
];
if scratch.is_some() {
changed_fields.push(ThreadEpiphanyStateUpdatedField::Scratch);
changed_fields.push(EpiphanyStateUpdatedField::Scratch);
}
if investigation_checkpoint.is_some() {
changed_fields.push(ThreadEpiphanyStateUpdatedField::InvestigationCheckpoint);
changed_fields.push(EpiphanyStateUpdatedField::InvestigationCheckpoint);
}
Ok(ReorientAcceptanceUpdate {
@ -284,100 +226,20 @@ pub fn build_reorient_acceptance_update(
accepted_receipt_id,
accepted_observation_id,
accepted_evidence_id,
mind_review,
})
}
pub fn role_finding_summary(finding: &ThreadEpiphanyRoleFinding) -> String {
let summary = finding
.summary
.clone()
.unwrap_or_else(|| "Role worker returned a structured finding.".to_string());
if let Some(next_safe_move) = finding.next_safe_move.as_deref() {
format!("{summary} Next safe move: {next_safe_move}")
} else {
summary
}
}
pub fn reorient_finding_code_refs(finding: &ThreadEpiphanyReorientFinding) -> Vec<EpiphanyCodeRef> {
finding
.files_inspected
.iter()
.filter(|path| !path.trim().is_empty())
.map(|path| EpiphanyCodeRef {
path: PathBuf::from(path),
start_line: None,
end_line: None,
symbol: None,
note: Some("Inspected by accepted reorientation worker.".to_string()),
})
.collect()
}
pub fn reorient_finding_scratch(
binding_id: &str,
finding: &ThreadEpiphanyReorientFinding,
) -> EpiphanyScratchPad {
let mode = finding.mode.as_deref().unwrap_or("unknown");
let checkpoint_validity = match finding.checkpoint_still_valid {
Some(true) => "valid",
Some(false) => "invalid",
None => "unknown",
};
EpiphanyScratchPad {
summary: finding.summary.clone(),
hypothesis: Some(format!(
"Accepted {mode} reorientation finding from {binding_id}; checkpoint validity is {checkpoint_validity}."
)),
next_probe: finding.next_safe_move.clone(),
notes: vec![format!(
"Files inspected: {}",
if finding.files_inspected.is_empty() {
"none reported".to_string()
} else {
finding.files_inspected.join(", ")
}
)],
}
}
pub fn reorient_finding_investigation_checkpoint(
checkpoint: &EpiphanyInvestigationCheckpoint,
evidence_id: &str,
code_refs: &[EpiphanyCodeRef],
finding: &ThreadEpiphanyReorientFinding,
) -> EpiphanyInvestigationCheckpoint {
let mut checkpoint = checkpoint.clone();
checkpoint.summary = finding.summary.clone().or(checkpoint.summary);
checkpoint.next_action = finding.next_safe_move.clone().or(checkpoint.next_action);
checkpoint.disposition = EpiphanyInvestigationDisposition::ResumeReady;
if !checkpoint
.evidence_ids
.iter()
.any(|existing| existing == evidence_id)
{
checkpoint.evidence_ids.push(evidence_id.to_string());
}
for code_ref in code_refs {
if !checkpoint
.code_refs
.iter()
.any(|existing| existing.path == code_ref.path)
{
checkpoint.code_refs.push(code_ref.clone());
}
}
checkpoint
}
pub fn epiphany_job_launch_changed_fields() -> Vec<ThreadEpiphanyStateUpdatedField> {
pub fn epiphany_job_launch_changed_fields() -> Vec<EpiphanyStateUpdatedField> {
vec![
ThreadEpiphanyStateUpdatedField::JobBindings,
ThreadEpiphanyStateUpdatedField::RuntimeLinks,
EpiphanyStateUpdatedField::JobBindings,
EpiphanyStateUpdatedField::RuntimeLinks,
]
}
pub fn thread_epiphany_patch_has_state_replacements(patch: &ThreadEpiphanyUpdatePatch) -> bool {
pub fn thread_epiphany_patch_has_state_replacements(
patch: &EpiphanyRoleStatePatchDocument,
) -> bool {
patch.objective.is_some()
|| patch.active_subgoal_id.is_some()
|| patch.subgoals.is_some()
@ -396,69 +258,69 @@ pub fn thread_epiphany_patch_has_state_replacements(patch: &ThreadEpiphanyUpdate
}
pub fn epiphany_update_patch_changed_fields(
patch: &ThreadEpiphanyUpdatePatch,
) -> Vec<ThreadEpiphanyStateUpdatedField> {
patch: &EpiphanyRoleStatePatchDocument,
) -> Vec<EpiphanyStateUpdatedField> {
let mut fields = Vec::new();
if patch.objective.is_some() {
fields.push(ThreadEpiphanyStateUpdatedField::Objective);
fields.push(EpiphanyStateUpdatedField::Objective);
}
if patch.active_subgoal_id.is_some() {
fields.push(ThreadEpiphanyStateUpdatedField::ActiveSubgoalId);
fields.push(EpiphanyStateUpdatedField::ActiveSubgoalId);
}
if patch.subgoals.is_some() {
fields.push(ThreadEpiphanyStateUpdatedField::Subgoals);
fields.push(EpiphanyStateUpdatedField::Subgoals);
}
if patch.invariants.is_some() {
fields.push(ThreadEpiphanyStateUpdatedField::Invariants);
fields.push(EpiphanyStateUpdatedField::Invariants);
}
if patch.graphs.is_some() {
fields.push(ThreadEpiphanyStateUpdatedField::Graphs);
fields.push(EpiphanyStateUpdatedField::Graphs);
}
if patch.graph_frontier.is_some() {
fields.push(ThreadEpiphanyStateUpdatedField::GraphFrontier);
fields.push(EpiphanyStateUpdatedField::GraphFrontier);
}
if patch.graph_checkpoint.is_some() {
fields.push(ThreadEpiphanyStateUpdatedField::GraphCheckpoint);
fields.push(EpiphanyStateUpdatedField::GraphCheckpoint);
}
if patch.scratch.is_some() {
fields.push(ThreadEpiphanyStateUpdatedField::Scratch);
fields.push(EpiphanyStateUpdatedField::Scratch);
}
if patch.investigation_checkpoint.is_some() {
fields.push(ThreadEpiphanyStateUpdatedField::InvestigationCheckpoint);
fields.push(EpiphanyStateUpdatedField::InvestigationCheckpoint);
}
if patch.job_bindings.is_some() {
fields.push(ThreadEpiphanyStateUpdatedField::JobBindings);
fields.push(EpiphanyStateUpdatedField::JobBindings);
}
if !patch.acceptance_receipts.is_empty() {
fields.push(ThreadEpiphanyStateUpdatedField::AcceptanceReceipts);
fields.push(EpiphanyStateUpdatedField::AcceptanceReceipts);
}
if !patch.runtime_links.is_empty() {
fields.push(ThreadEpiphanyStateUpdatedField::RuntimeLinks);
fields.push(EpiphanyStateUpdatedField::RuntimeLinks);
}
if !patch.observations.is_empty() {
fields.push(ThreadEpiphanyStateUpdatedField::Observations);
fields.push(EpiphanyStateUpdatedField::Observations);
}
if !patch.evidence.is_empty() {
fields.push(ThreadEpiphanyStateUpdatedField::Evidence);
fields.push(EpiphanyStateUpdatedField::Evidence);
}
if patch.churn.is_some() {
fields.push(ThreadEpiphanyStateUpdatedField::Churn);
fields.push(EpiphanyStateUpdatedField::Churn);
}
if patch.mode.is_some() {
fields.push(ThreadEpiphanyStateUpdatedField::Mode);
fields.push(EpiphanyStateUpdatedField::Mode);
}
if patch.planning.is_some() {
fields.push(ThreadEpiphanyStateUpdatedField::Planning);
fields.push(EpiphanyStateUpdatedField::Planning);
}
fields
}
pub fn epiphany_promote_changed_fields(
patch: &ThreadEpiphanyUpdatePatch,
) -> Vec<ThreadEpiphanyStateUpdatedField> {
patch: &EpiphanyRoleStatePatchDocument,
) -> Vec<EpiphanyStateUpdatedField> {
let mut fields = epiphany_update_patch_changed_fields(patch);
if !fields.contains(&ThreadEpiphanyStateUpdatedField::Evidence) {
fields.push(ThreadEpiphanyStateUpdatedField::Evidence);
if !fields.contains(&EpiphanyStateUpdatedField::Evidence) {
fields.push(EpiphanyStateUpdatedField::Evidence);
}
fields
}

View file

@ -1,29 +1,42 @@
use chrono::SecondsFormat;
use chrono::Utc;
use codex_app_server_protocol::ThreadEpiphanyJob;
use codex_app_server_protocol::ThreadEpiphanyJobKind;
use codex_app_server_protocol::ThreadEpiphanyReorientDecision;
use codex_app_server_protocol::ThreadEpiphanyReorientFinding;
use codex_app_server_protocol::ThreadEpiphanyReorientSource;
use codex_app_server_protocol::ThreadEpiphanyReorientStateStatus;
use codex_app_server_protocol::ThreadEpiphanyRoleFinding;
use codex_app_server_protocol::ThreadEpiphanyRoleId;
use codex_app_server_protocol::ThreadEpiphanyStateUpdatedField;
use codex_app_server_protocol::ThreadEpiphanyUpdatePatch;
use codex_core::CodexThread;
use codex_protocol::error::CodexErr;
use codex_protocol::protocol::EpiphanyEvidenceRecord;
use codex_protocol::protocol::EpiphanyJobKind as CoreEpiphanyJobKind;
use codex_protocol::protocol::EpiphanyRetrievalState;
use codex_protocol::protocol::EpiphanyThreadState;
use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo;
use epiphany_core::EpiphanyJobInterruptRequest;
use epiphany_core::EpiphanyJobLaunchRequest;
use epiphany_core::EpiphanyPromotionInput;
use epiphany_core::evaluate_promotion;
use std::path::Path;
use std::path::PathBuf;
use epiphany_core::EpiphanyJobInterruptRequest;
use epiphany_core::EpiphanyJobInterruptResult;
use epiphany_core::EpiphanyJobLaunchRequest;
use epiphany_core::EpiphanyJobLaunchResult;
use epiphany_core::EpiphanyJobView;
use epiphany_core::EpiphanyPromotionInput;
use epiphany_core::EpiphanyReorientDecision;
use epiphany_core::EpiphanyReorientFindingInterpretation;
use epiphany_core::EpiphanyReorientStateStatus;
use epiphany_core::EpiphanyRoleFindingInterpretation;
use epiphany_core::EpiphanyRoleResultRoleId;
use epiphany_core::EpiphanyRoleStatePatchDocument;
use epiphany_core::EpiphanyStateUpdate;
use epiphany_core::EpiphanyStateUpdatedField;
use epiphany_core::EpiphanyTokenUsageSnapshot;
use epiphany_core::RuntimeSpineHeartbeatJobOptions;
use epiphany_core::RuntimeSpineHeartbeatLaunchPlanOptions;
use epiphany_core::apply_epiphany_state_update;
use epiphany_core::clear_epiphany_job_binding_backend;
use epiphany_core::epiphany_state_update_validation_errors;
use epiphany_core::evaluate_promotion;
use epiphany_core::open_runtime_spine_heartbeat_job;
use epiphany_core::plan_runtime_spine_heartbeat_launch;
use epiphany_core::replace_or_append_epiphany_job_binding;
use epiphany_state_model::EpiphanyEvidenceRecord;
use epiphany_state_model::EpiphanyJobKind as CoreEpiphanyJobKind;
use epiphany_state_model::EpiphanyRetrievalState;
use epiphany_state_model::EpiphanyThreadState;
use crate::cultnet::EpiphanySurfaceSource;
use crate::error::EpiphanyBridgeError;
use crate::error::Result as BridgeResult;
use crate::jobs::derive_epiphany_jobs;
use crate::jobs::epiphany_blocked_state_job;
use crate::jobs::map_epiphany_jobs;
use crate::jobs::map_interrupted_epiphany_job;
use crate::jobs::map_launched_epiphany_job;
use crate::launch::EPIPHANY_REORIENT_LAUNCH_BINDING_ID;
@ -35,21 +48,44 @@ use crate::mutation::build_role_acceptance_update;
use crate::mutation::epiphany_job_launch_changed_fields;
use crate::mutation::epiphany_promote_changed_fields;
use crate::mutation::epiphany_update_patch_changed_fields;
use crate::mutation::state_update_from_thread_patch;
use crate::mutation::thread_epiphany_patch_has_state_replacements;
use crate::pressure::map_epiphany_pressure;
use crate::pressure::derive_epiphany_pressure;
use crate::reorient::EpiphanyFreshnessWatcherSnapshot;
use crate::reorient::map_epiphany_freshness;
use crate::reorient::map_epiphany_reorient;
use crate::runtime_results::load_completed_epiphany_reorient_finding;
use crate::runtime_results::load_completed_epiphany_role_finding;
use crate::state::client_visible_live_thread_epiphany_state;
use crate::reorient::derive_epiphany_freshness_view;
use crate::reorient::derive_epiphany_reorient;
use crate::runtime_results::load_completed_core_epiphany_reorient_finding;
use crate::runtime_results::load_completed_core_epiphany_role_finding;
use uuid::Uuid;
#[allow(async_fn_in_trait)]
pub trait EpiphanyMutationHost {
/// Read the Codex-hosted compatibility snapshot.
///
/// The bridge may request host state so core can evaluate typed policy. The
/// host snapshot is an input fact, not a bridge-owned source of truth.
async fn epiphany_state(&self) -> Option<EpiphanyThreadState>;
async fn epiphany_reference_turn_id(&self) -> Option<String>;
/// Persist a state document already validated by Epiphany core policy.
///
/// This is the Codex JSON-RPC shell's current persistence hook. It must not
/// grow mutation policy of its own.
async fn epiphany_persist_state(
&self,
next_state: EpiphanyThreadState,
) -> BridgeResult<EpiphanyThreadState>;
/// Locate the native runtime-spine store while Codex still hosts the turn.
async fn epiphany_runtime_spine_store_path(&self) -> PathBuf;
/// Project the already-typed state into Codex's current client-visible view.
async fn client_visible_epiphany_state(
&self,
fallback: EpiphanyThreadState,
) -> EpiphanyThreadState;
}
#[derive(Debug, Clone)]
pub struct EpiphanyThreadUpdateApplied {
pub revision: u64,
pub changed_fields: Vec<ThreadEpiphanyStateUpdatedField>,
pub changed_fields: Vec<EpiphanyStateUpdatedField>,
pub epiphany_state: EpiphanyThreadState,
}
@ -62,32 +98,34 @@ pub enum EpiphanyThreadPromoteApplied {
#[derive(Debug, Clone)]
pub struct EpiphanyRoleAcceptApplied {
pub revision: u64,
pub changed_fields: Vec<ThreadEpiphanyStateUpdatedField>,
pub changed_fields: Vec<EpiphanyStateUpdatedField>,
pub epiphany_state: EpiphanyThreadState,
pub accepted_receipt_id: String,
pub accepted_observation_id: String,
pub accepted_evidence_id: String,
pub applied_patch: ThreadEpiphanyUpdatePatch,
pub finding: ThreadEpiphanyRoleFinding,
pub applied_patch: EpiphanyRoleStatePatchDocument,
pub finding: EpiphanyRoleFindingInterpretation,
}
#[derive(Debug, Clone)]
pub struct EpiphanyReorientAcceptApplied {
pub revision: u64,
pub changed_fields: Vec<ThreadEpiphanyStateUpdatedField>,
pub changed_fields: Vec<EpiphanyStateUpdatedField>,
pub epiphany_state: EpiphanyThreadState,
pub accepted_receipt_id: String,
pub accepted_observation_id: String,
pub accepted_evidence_id: String,
pub finding: ThreadEpiphanyReorientFinding,
pub finding: EpiphanyReorientFindingInterpretation,
}
#[derive(Debug, Clone)]
pub struct EpiphanyJobLaunchApplied {
pub launcher_job_id: String,
pub backend_job_id: String,
pub revision: u64,
pub changed_fields: Vec<ThreadEpiphanyStateUpdatedField>,
pub changed_fields: Vec<EpiphanyStateUpdatedField>,
pub epiphany_state: EpiphanyThreadState,
pub job: ThreadEpiphanyJob,
pub job: EpiphanyJobView,
}
#[derive(Debug, Clone)]
@ -95,32 +133,203 @@ pub struct EpiphanyJobInterruptApplied {
pub cancel_requested: bool,
pub interrupted_thread_ids: Vec<String>,
pub revision: u64,
pub changed_fields: Vec<ThreadEpiphanyStateUpdatedField>,
pub changed_fields: Vec<EpiphanyStateUpdatedField>,
pub epiphany_state: EpiphanyThreadState,
pub job: ThreadEpiphanyJob,
pub job: EpiphanyJobView,
}
#[derive(Debug, Clone)]
pub struct EpiphanyReorientLaunchApplied {
pub source: ThreadEpiphanyReorientSource,
pub state_status: ThreadEpiphanyReorientStateStatus,
pub launcher_job_id: String,
pub backend_job_id: String,
pub source: EpiphanySurfaceSource,
pub state_status: EpiphanyReorientStateStatus,
pub state_revision: Option<u64>,
pub decision: ThreadEpiphanyReorientDecision,
pub decision: EpiphanyReorientDecision,
pub revision: u64,
pub changed_fields: Vec<ThreadEpiphanyStateUpdatedField>,
pub changed_fields: Vec<EpiphanyStateUpdatedField>,
pub epiphany_state: EpiphanyThreadState,
pub job: ThreadEpiphanyJob,
pub job: EpiphanyJobView,
}
pub async fn apply_epiphany_state_update_to_thread(
thread: &impl EpiphanyMutationHost,
update: EpiphanyStateUpdate,
) -> BridgeResult<EpiphanyThreadState> {
let current_state = thread.epiphany_state().await.unwrap_or_default();
let reference_turn_id = thread.epiphany_reference_turn_id().await;
let next_state =
apply_epiphany_state_update_to_state(&current_state, update, reference_turn_id)?;
thread.epiphany_persist_state(next_state).await
}
pub fn apply_epiphany_state_update_to_state(
current_state: &EpiphanyThreadState,
update: EpiphanyStateUpdate,
reference_turn_id: Option<String>,
) -> BridgeResult<EpiphanyThreadState> {
if update.is_empty() {
return Err(EpiphanyBridgeError::InvalidRequest(
"epiphany update patch must contain at least one mutation".to_string(),
));
}
let mut next_state = current_state.clone();
if let Some(expected_revision) = update.expected_revision
&& next_state.revision != expected_revision
{
return Err(EpiphanyBridgeError::InvalidRequest(format!(
"epiphany state revision mismatch: expected {expected_revision}, found {}",
next_state.revision
)));
}
let validation_errors = epiphany_state_update_validation_errors(&next_state, &update);
if !validation_errors.is_empty() {
return Err(EpiphanyBridgeError::InvalidRequest(format!(
"invalid epiphany update patch: {}",
validation_errors.join("; ")
)));
}
apply_epiphany_state_update(&mut next_state, update, reference_turn_id);
Ok(next_state)
}
pub async fn launch_epiphany_job_on_thread(
thread: &impl EpiphanyMutationHost,
request: EpiphanyJobLaunchRequest,
) -> BridgeResult<EpiphanyJobLaunchResult> {
// Compatibility choreography only: core validates the launch plan and
// runtime-spine opens the typed job. The bridge stitches those typed
// documents into Codex's JSON-RPC thread shell until CultNet owns the route.
let current_state = thread.epiphany_state().await.unwrap_or_default();
validate_expected_revision(request.expected_revision, current_state.revision)?;
let launcher_job_id = format!("epiphany-heartbeat-launch-{}", Uuid::new_v4());
let backend_job_id = Uuid::new_v4().to_string();
let launch_plan = plan_runtime_spine_heartbeat_launch(
&current_state,
RuntimeSpineHeartbeatLaunchPlanOptions {
binding_id: request.binding_id.clone(),
kind: request.kind,
scope: request.scope.clone(),
owner_role: request.owner_role.clone(),
authority_scope: request.authority_scope.clone(),
linked_subgoal_ids: request.linked_subgoal_ids.clone(),
linked_graph_node_ids: request.linked_graph_node_ids.clone(),
instruction: request.instruction.clone(),
launch_document: request.launch_document.clone(),
output_contract_id: request.output_contract_id.clone(),
max_runtime_seconds: request.max_runtime_seconds,
runtime_job_id: backend_job_id.clone(),
},
)
.map_err(|err| EpiphanyBridgeError::InvalidRequest(err.to_string()))?;
let runtime_store = thread.epiphany_runtime_spine_store_path().await;
open_epiphany_runtime_spine_job(
runtime_store.as_path(),
&current_state,
&request,
backend_job_id.as_str(),
)?;
let next_job_bindings = replace_or_append_epiphany_job_binding(
current_state.job_bindings.clone(),
launch_plan.binding,
);
let validation_errors = epiphany_state_update_validation_errors(
&current_state,
&EpiphanyStateUpdate {
job_bindings: Some(next_job_bindings.clone()),
runtime_links: vec![launch_plan.runtime_link.clone()],
..Default::default()
},
);
if !validation_errors.is_empty() {
return Err(EpiphanyBridgeError::InvalidRequest(format!(
"invalid Epiphany job launch patch: {}",
validation_errors.join("; ")
)));
}
let epiphany_state = apply_epiphany_state_update_to_thread(
thread,
EpiphanyStateUpdate {
expected_revision: request.expected_revision,
job_bindings: Some(next_job_bindings),
runtime_links: vec![launch_plan.runtime_link],
..Default::default()
},
)
.await?;
Ok(EpiphanyJobLaunchResult {
epiphany_state,
binding_id: request.binding_id,
launcher_job_id,
backend_job_id,
})
}
pub async fn interrupt_epiphany_job_on_thread(
thread: &impl EpiphanyMutationHost,
request: EpiphanyJobInterruptRequest,
) -> BridgeResult<EpiphanyJobInterruptResult> {
if request.binding_id.trim().is_empty() {
return Err(EpiphanyBridgeError::InvalidRequest(
"epiphany job interrupt binding_id must be non-empty".to_string(),
));
}
let current_state = thread.epiphany_state().await.unwrap_or_default();
validate_expected_revision(request.expected_revision, current_state.revision)?;
let Some(binding_index) = current_state
.job_bindings
.iter()
.position(|binding| binding.id == request.binding_id)
else {
return Err(EpiphanyBridgeError::InvalidRequest(format!(
"epiphany job binding {:?} was not found",
request.binding_id
)));
};
let interrupted_thread_ids = Vec::new();
let cancel_requested = false;
let next_job_bindings = clear_epiphany_job_binding_backend(
current_state.job_bindings.clone(),
binding_index,
"No active heartbeat turn is currently bound; launch explicitly to resume specialist work.",
);
let epiphany_state = apply_epiphany_state_update_to_thread(
thread,
EpiphanyStateUpdate {
expected_revision: request.expected_revision,
job_bindings: Some(next_job_bindings),
..Default::default()
},
)
.await?;
Ok(EpiphanyJobInterruptResult {
epiphany_state,
binding_id: request.binding_id,
cancel_requested,
interrupted_thread_ids,
})
}
pub async fn apply_thread_epiphany_update(
thread: &CodexThread,
thread: &impl EpiphanyMutationHost,
expected_revision: Option<u64>,
patch: ThreadEpiphanyUpdatePatch,
) -> Result<EpiphanyThreadUpdateApplied, CodexErr> {
patch: EpiphanyRoleStatePatchDocument,
) -> BridgeResult<EpiphanyThreadUpdateApplied> {
let changed_fields = epiphany_update_patch_changed_fields(&patch);
let update = state_update_from_thread_patch(expected_revision, patch);
let epiphany_state = thread.epiphany_update_state(update).await?;
let epiphany_state = client_visible_live_thread_epiphany_state(thread, epiphany_state).await;
let update = crate::mutation::state_update_from_core_patch(expected_revision, patch);
let epiphany_state = apply_epiphany_state_update_to_thread(thread, update).await?;
let epiphany_state = thread.client_visible_epiphany_state(epiphany_state).await;
Ok(EpiphanyThreadUpdateApplied {
revision: epiphany_state.revision,
changed_fields,
@ -129,11 +338,11 @@ pub async fn apply_thread_epiphany_update(
}
pub async fn apply_thread_epiphany_promote(
thread: &CodexThread,
thread: &impl EpiphanyMutationHost,
expected_revision: Option<u64>,
patch: ThreadEpiphanyUpdatePatch,
patch: EpiphanyRoleStatePatchDocument,
verifier_evidence: EpiphanyEvidenceRecord,
) -> Result<EpiphanyThreadPromoteApplied, CodexErr> {
) -> BridgeResult<EpiphanyThreadPromoteApplied> {
let decision = evaluate_promotion(EpiphanyPromotionInput {
has_state_replacements: thread_epiphany_patch_has_state_replacements(&patch),
active_subgoal_id: patch.active_subgoal_id.clone(),
@ -157,9 +366,9 @@ pub async fn apply_thread_epiphany_promote(
let changed_fields = epiphany_promote_changed_fields(&patch);
let mut patch = patch;
patch.evidence.push(verifier_evidence);
let update = state_update_from_thread_patch(expected_revision, patch);
let epiphany_state = thread.epiphany_update_state(update).await?;
let epiphany_state = client_visible_live_thread_epiphany_state(thread, epiphany_state).await;
let update = crate::mutation::state_update_from_core_patch(expected_revision, patch);
let epiphany_state = apply_epiphany_state_update_to_thread(thread, update).await?;
let epiphany_state = thread.client_visible_epiphany_state(epiphany_state).await;
Ok(EpiphanyThreadPromoteApplied::Accepted(
EpiphanyThreadUpdateApplied {
revision: epiphany_state.revision,
@ -170,22 +379,29 @@ pub async fn apply_thread_epiphany_promote(
}
pub async fn apply_thread_epiphany_role_accept(
thread: &CodexThread,
role_id: ThreadEpiphanyRoleId,
thread: &impl EpiphanyMutationHost,
role_id: EpiphanyRoleResultRoleId,
expected_revision: Option<u64>,
binding_id: &str,
) -> Result<EpiphanyRoleAcceptApplied, CodexErr> {
) -> BridgeResult<EpiphanyRoleAcceptApplied> {
let state = thread.epiphany_state().await.ok_or_else(|| {
CodexErr::InvalidRequest(
EpiphanyBridgeError::InvalidRequest(
"cannot accept an Epiphany role finding without authoritative Epiphany state"
.to_string(),
)
})?;
validate_expected_revision(expected_revision, state.revision)?;
let finding = load_completed_epiphany_role_finding(thread, &state, role_id, binding_id).await?;
let runtime_store_path = thread.epiphany_runtime_spine_store_path().await;
let finding = load_completed_core_epiphany_role_finding(
Some(runtime_store_path.as_path()),
&state,
role_id,
binding_id,
)?;
let accepted_prefix = epiphany_role_label(role_id);
let acceptance_update = build_role_acceptance_update(
expected_revision,
role_id,
binding_id,
&finding,
@ -193,20 +409,16 @@ pub async fn apply_thread_epiphany_role_accept(
format!("obs-{accepted_prefix}-{}", Uuid::new_v4()),
Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
)
.map_err(CodexErr::InvalidRequest)?;
.map_err(EpiphanyBridgeError::InvalidRequest)?;
let accepted_receipt_id = acceptance_update.accepted_receipt_id.clone();
let accepted_observation_id = acceptance_update.accepted_observation_id.clone();
let accepted_evidence_id = acceptance_update.accepted_evidence_id.clone();
let changed_fields = acceptance_update.changed_fields.clone();
let applied_patch = acceptance_update.patch.clone();
let epiphany_state = thread
.epiphany_update_state(state_update_from_thread_patch(
expected_revision,
acceptance_update.patch,
))
.await?;
let epiphany_state = client_visible_live_thread_epiphany_state(thread, epiphany_state).await;
let applied_patch = acceptance_update.applied_patch.clone();
let epiphany_state =
apply_epiphany_state_update_to_thread(thread, acceptance_update.state_update).await?;
let epiphany_state = thread.client_visible_epiphany_state(epiphany_state).await;
Ok(EpiphanyRoleAcceptApplied {
revision: epiphany_state.revision,
@ -221,27 +433,32 @@ pub async fn apply_thread_epiphany_role_accept(
}
pub async fn apply_thread_epiphany_reorient_accept(
thread: &CodexThread,
thread: &impl EpiphanyMutationHost,
expected_revision: Option<u64>,
binding_id: &str,
update_scratch: bool,
update_investigation_checkpoint: bool,
) -> Result<EpiphanyReorientAcceptApplied, CodexErr> {
) -> BridgeResult<EpiphanyReorientAcceptApplied> {
let state = thread.epiphany_state().await.ok_or_else(|| {
CodexErr::InvalidRequest(
EpiphanyBridgeError::InvalidRequest(
"cannot accept a reorientation finding without authoritative Epiphany state"
.to_string(),
)
})?;
validate_expected_revision(expected_revision, state.revision)?;
if update_investigation_checkpoint && state.investigation_checkpoint.is_none() {
return Err(CodexErr::InvalidRequest(
return Err(EpiphanyBridgeError::InvalidRequest(
"cannot update investigation checkpoint because this thread has no durable checkpoint"
.to_string(),
));
}
let finding = load_completed_epiphany_reorient_finding(thread, &state, binding_id).await?;
let runtime_store_path = thread.epiphany_runtime_spine_store_path().await;
let finding = load_completed_core_epiphany_reorient_finding(
Some(runtime_store_path.as_path()),
&state,
binding_id,
)?;
let acceptance_update = build_reorient_acceptance_update(
expected_revision,
binding_id,
@ -253,16 +470,15 @@ pub async fn apply_thread_epiphany_reorient_accept(
update_investigation_checkpoint,
state.investigation_checkpoint.clone(),
)
.map_err(CodexErr::InvalidRequest)?;
.map_err(EpiphanyBridgeError::InvalidRequest)?;
let accepted_receipt_id = acceptance_update.accepted_receipt_id.clone();
let accepted_observation_id = acceptance_update.accepted_observation_id.clone();
let accepted_evidence_id = acceptance_update.accepted_evidence_id.clone();
let changed_fields = acceptance_update.changed_fields.clone();
let epiphany_state = thread
.epiphany_update_state(acceptance_update.state_update)
.await?;
let epiphany_state = client_visible_live_thread_epiphany_state(thread, epiphany_state).await;
let epiphany_state =
apply_epiphany_state_update_to_thread(thread, acceptance_update.state_update).await?;
let epiphany_state = thread.client_visible_epiphany_state(epiphany_state).await;
Ok(EpiphanyReorientAcceptApplied {
revision: epiphany_state.revision,
@ -276,14 +492,14 @@ pub async fn apply_thread_epiphany_reorient_accept(
}
pub async fn launch_thread_epiphany_role(
thread: &CodexThread,
thread: &impl EpiphanyMutationHost,
thread_id: &str,
role_id: ThreadEpiphanyRoleId,
role_id: EpiphanyRoleResultRoleId,
expected_revision: Option<u64>,
max_runtime_seconds: Option<u64>,
) -> Result<EpiphanyJobLaunchApplied, CodexErr> {
) -> BridgeResult<EpiphanyJobLaunchApplied> {
let state = thread.epiphany_state().await.ok_or_else(|| {
CodexErr::InvalidRequest(
EpiphanyBridgeError::InvalidRequest(
"cannot launch an Epiphany role specialist without authoritative Epiphany state"
.to_string(),
)
@ -295,7 +511,7 @@ pub async fn launch_thread_epiphany_role(
max_runtime_seconds,
&state,
)
.map_err(CodexErr::InvalidRequest)?;
.map_err(EpiphanyBridgeError::InvalidRequest)?;
launch_thread_epiphany_job(
thread,
launch_request,
@ -306,24 +522,26 @@ pub async fn launch_thread_epiphany_role(
}
pub async fn launch_thread_epiphany_job(
thread: &CodexThread,
thread: &impl EpiphanyMutationHost,
launch_request: EpiphanyJobLaunchRequest,
kind: CoreEpiphanyJobKind,
missing_projection_reason: &str,
) -> Result<EpiphanyJobLaunchApplied, CodexErr> {
) -> BridgeResult<EpiphanyJobLaunchApplied> {
let changed_fields = epiphany_job_launch_changed_fields();
let launched = thread.epiphany_launch_job(launch_request).await?;
let epiphany_state =
client_visible_live_thread_epiphany_state(thread, launched.epiphany_state).await;
let launched = launch_epiphany_job_on_thread(thread, launch_request).await?;
let epiphany_state = thread
.client_visible_epiphany_state(launched.epiphany_state)
.await;
let job = map_launched_epiphany_job(
&epiphany_state,
launched.binding_id.as_str(),
launched.launcher_job_id.as_str(),
launched.backend_job_id.as_str(),
kind,
missing_projection_reason,
);
Ok(EpiphanyJobLaunchApplied {
launcher_job_id: launched.launcher_job_id,
backend_job_id: launched.backend_job_id,
revision: epiphany_state.revision,
changed_fields,
epiphany_state,
@ -332,21 +550,24 @@ pub async fn launch_thread_epiphany_job(
}
pub async fn interrupt_thread_epiphany_job(
thread: &CodexThread,
thread: &impl EpiphanyMutationHost,
expected_revision: Option<u64>,
binding_id: &str,
reason: Option<String>,
) -> Result<EpiphanyJobInterruptApplied, CodexErr> {
let changed_fields = vec![ThreadEpiphanyStateUpdatedField::JobBindings];
let interrupted = thread
.epiphany_interrupt_job(EpiphanyJobInterruptRequest {
) -> BridgeResult<EpiphanyJobInterruptApplied> {
let changed_fields = vec![EpiphanyStateUpdatedField::JobBindings];
let interrupted = interrupt_epiphany_job_on_thread(
thread,
EpiphanyJobInterruptRequest {
expected_revision,
binding_id: binding_id.to_string(),
reason,
})
.await?;
let epiphany_state =
client_visible_live_thread_epiphany_state(thread, interrupted.epiphany_state).await;
},
)
.await?;
let epiphany_state = thread
.client_visible_epiphany_state(interrupted.epiphany_state)
.await;
let job = map_interrupted_epiphany_job(&epiphany_state, binding_id);
Ok(EpiphanyJobInterruptApplied {
@ -360,29 +581,34 @@ pub async fn interrupt_thread_epiphany_job(
}
pub async fn launch_thread_epiphany_reorient(
thread: &CodexThread,
thread: &impl EpiphanyMutationHost,
thread_id: &str,
expected_revision: Option<u64>,
max_runtime_seconds: Option<u64>,
state: Option<&EpiphanyThreadState>,
retrieval_override: Option<&EpiphanyRetrievalState>,
watcher_snapshot: Option<EpiphanyFreshnessWatcherSnapshot<'_>>,
token_usage_info: Option<&CoreTokenUsageInfo>,
) -> Result<EpiphanyReorientLaunchApplied, CodexErr> {
let (state_revision, retrieval, graph, watcher) =
map_epiphany_freshness(state, retrieval_override, watcher_snapshot);
let pressure = map_epiphany_pressure(token_usage_info);
let (state_status, decision) =
map_epiphany_reorient(state, &pressure, &retrieval, &graph, &watcher);
token_usage_info: Option<&EpiphanyTokenUsageSnapshot>,
) -> BridgeResult<EpiphanyReorientLaunchApplied> {
let freshness = derive_epiphany_freshness_view(state, retrieval_override, watcher_snapshot);
let state_revision = freshness.state_revision;
let pressure = derive_epiphany_pressure(token_usage_info);
let (state_status, decision) = derive_epiphany_reorient(
state,
&pressure,
&freshness.retrieval,
&freshness.graph,
&freshness.watcher,
);
let state = state.ok_or_else(|| {
CodexErr::InvalidRequest(format!(
EpiphanyBridgeError::InvalidRequest(format!(
"cannot launch a reorientation worker without authoritative Epiphany state: {}",
decision.note
))
})?;
let checkpoint = state.investigation_checkpoint.as_ref().ok_or_else(|| {
CodexErr::InvalidRequest(format!(
EpiphanyBridgeError::InvalidRequest(format!(
"cannot launch a reorientation worker without a durable investigation checkpoint: {}",
decision.note
))
@ -397,23 +623,26 @@ pub async fn launch_thread_epiphany_reorient(
&decision,
);
let changed_fields = epiphany_job_launch_changed_fields();
let launched = thread.epiphany_launch_job(launch_request).await?;
let epiphany_state =
client_visible_live_thread_epiphany_state(thread, launched.epiphany_state).await;
let job = map_epiphany_jobs(Some(&epiphany_state), None)
let launched = launch_epiphany_job_on_thread(thread, launch_request).await?;
let epiphany_state = thread
.client_visible_epiphany_state(launched.epiphany_state)
.await;
let job = derive_epiphany_jobs(Some(&epiphany_state), None)
.into_iter()
.find(|job| job.id == EPIPHANY_REORIENT_LAUNCH_BINDING_ID)
.unwrap_or_else(|| {
epiphany_blocked_state_job(
EPIPHANY_REORIENT_LAUNCH_BINDING_ID,
ThreadEpiphanyJobKind::Specialist,
CoreEpiphanyJobKind::Specialist,
"reorient-guided checkpoint regather",
"Launched reorientation worker was not reflected in Epiphany state.",
)
});
Ok(EpiphanyReorientLaunchApplied {
source: ThreadEpiphanyReorientSource::Live,
launcher_job_id: launched.launcher_job_id,
backend_job_id: launched.backend_job_id,
source: EpiphanySurfaceSource::Live,
state_status,
state_revision,
decision,
@ -424,14 +653,53 @@ pub async fn launch_thread_epiphany_reorient(
})
}
fn open_epiphany_runtime_spine_job(
store_path: &Path,
state: &EpiphanyThreadState,
request: &EpiphanyJobLaunchRequest,
backend_job_id: &str,
) -> BridgeResult<()> {
let now = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
open_runtime_spine_heartbeat_job(
store_path,
RuntimeSpineHeartbeatJobOptions {
runtime_id: "epiphany-local".to_string(),
display_name: "Epiphany Local".to_string(),
session_id: "epiphany-main".to_string(),
objective: state
.objective
.clone()
.filter(|objective| !objective.trim().is_empty())
.unwrap_or_else(|| "Epiphany heartbeat activation".to_string()),
coordinator_note: "App-server launch opened this typed runtime session.".to_string(),
job_id: backend_job_id.to_string(),
role: request.owner_role.clone(),
binding_id: request.binding_id.clone(),
authority_scope: request.authority_scope.clone(),
instruction: request.instruction.clone(),
launch_document: request.launch_document.clone(),
output_contract_id: request.output_contract_id.clone(),
created_at: now,
},
)
.map_err(|err| {
EpiphanyBridgeError::Fatal(format!(
"failed to open Epiphany runtime spine job {:?} in {}: {err}",
backend_job_id,
store_path.display()
))
})?;
Ok(())
}
fn validate_expected_revision(
expected_revision: Option<u64>,
actual_revision: u64,
) -> Result<(), CodexErr> {
) -> BridgeResult<()> {
if let Some(expected_revision) = expected_revision
&& actual_revision != expected_revision
{
return Err(CodexErr::InvalidRequest(format!(
return Err(EpiphanyBridgeError::InvalidRequest(format!(
"epiphany state revision mismatch: expected {expected_revision}, found {actual_revision}"
)));
}

View file

@ -1,69 +1,24 @@
use codex_app_server_protocol::ThreadEpiphanyPressure;
use codex_app_server_protocol::ThreadEpiphanyPressureBasis;
use codex_app_server_protocol::ThreadEpiphanyPressureLevel;
use codex_app_server_protocol::ThreadEpiphanyPressureStatus;
use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo;
use epiphany_core::EpiphanyPressure;
use epiphany_core::EpiphanyPressureBasis as CoreEpiphanyPressureBasis;
use epiphany_core::EpiphanyPressureLevel as CoreEpiphanyPressureLevel;
use epiphany_core::EpiphanyPressureStatus as CoreEpiphanyPressureStatus;
use epiphany_core::EpiphanyTokenUsageSnapshot;
pub use epiphany_core::EpiphanyTokenUsageSnapshot;
use epiphany_core::derive_pressure_view;
use crate::launch::epiphany_agent_prompt_with_memory;
use crate::launch::epiphany_specialist_prompt_config;
use crate::launch::pressure_level_label;
pub fn map_epiphany_pressure(info: Option<&CoreTokenUsageInfo>) -> ThreadEpiphanyPressure {
let snapshot = info.map(|info| EpiphanyTokenUsageSnapshot {
total_tokens: info.total_token_usage.total_tokens,
last_turn_tokens: info.last_token_usage.total_tokens,
model_context_window: info.model_context_window,
model_auto_compact_token_limit: info.model_auto_compact_token_limit,
});
map_core_epiphany_pressure(derive_pressure_view(snapshot.as_ref()))
}
fn map_core_epiphany_pressure(pressure: EpiphanyPressure) -> ThreadEpiphanyPressure {
ThreadEpiphanyPressure {
status: match pressure.status {
CoreEpiphanyPressureStatus::Unknown => ThreadEpiphanyPressureStatus::Unknown,
CoreEpiphanyPressureStatus::Ready => ThreadEpiphanyPressureStatus::Ready,
},
level: match pressure.level {
CoreEpiphanyPressureLevel::Unknown => ThreadEpiphanyPressureLevel::Unknown,
CoreEpiphanyPressureLevel::Low => ThreadEpiphanyPressureLevel::Low,
CoreEpiphanyPressureLevel::Elevated => ThreadEpiphanyPressureLevel::Elevated,
CoreEpiphanyPressureLevel::High => ThreadEpiphanyPressureLevel::High,
CoreEpiphanyPressureLevel::Critical => ThreadEpiphanyPressureLevel::Critical,
},
basis: match pressure.basis {
CoreEpiphanyPressureBasis::Unknown => ThreadEpiphanyPressureBasis::Unknown,
CoreEpiphanyPressureBasis::AutoCompactLimit => {
ThreadEpiphanyPressureBasis::AutoCompactLimit
}
CoreEpiphanyPressureBasis::ModelContextWindow => {
ThreadEpiphanyPressureBasis::ModelContextWindow
}
},
used_tokens: pressure.used_tokens,
model_context_window: pressure.model_context_window,
model_auto_compact_token_limit: pressure.model_auto_compact_token_limit,
remaining_tokens: pressure.remaining_tokens,
ratio_per_mille: pressure.ratio_per_mille,
should_prepare_compaction: pressure.should_prepare_compaction,
note: pressure.note,
}
pub fn derive_epiphany_pressure(snapshot: Option<&EpiphanyTokenUsageSnapshot>) -> EpiphanyPressure {
derive_pressure_view(snapshot)
}
pub fn should_run_epiphany_pre_compaction_checkpoint_intervention(
pressure: &ThreadEpiphanyPressure,
pressure: &EpiphanyPressure,
) -> bool {
pressure.status == ThreadEpiphanyPressureStatus::Ready && pressure.should_prepare_compaction
pressure.status == CoreEpiphanyPressureStatus::Ready && pressure.should_prepare_compaction
}
pub fn render_epiphany_pre_compaction_checkpoint_intervention(
pressure: &ThreadEpiphanyPressure,
pressure: &EpiphanyPressure,
) -> String {
let usage = match (
pressure.used_tokens,
@ -85,6 +40,19 @@ pub fn render_epiphany_pre_compaction_checkpoint_intervention(
);
template
.trim()
.replace("{pressure_level}", pressure_level_label(pressure.level))
.replace(
"{pressure_level}",
core_pressure_level_label(pressure.level),
)
.replace("{usage}", &usage)
}
fn core_pressure_level_label(level: CoreEpiphanyPressureLevel) -> &'static str {
match level {
CoreEpiphanyPressureLevel::Unknown => "unknown",
CoreEpiphanyPressureLevel::Low => "low",
CoreEpiphanyPressureLevel::Elevated => "elevated",
CoreEpiphanyPressureLevel::High => "high",
CoreEpiphanyPressureLevel::Critical => "critical",
}
}

View file

@ -188,13 +188,8 @@ Implementation rules:
- Environment guardrails:
{unity_guidance}
{rider_guidance}
- Make a bounded first pass: design or implement a parallel compute-backed gravity height producer that preserves the current `_NebulaSurfaceHeight`, `_GridTransform`, and Slime `Heightmap` contracts before changing shader consumers.
- This is an implementation turn, not a planning turn. Leave a concrete source diff unless direct source evidence proves the bounded pass is impossible.
- If there is no implementation diff yet, prefer a small reversible compatibility scaffold over another explanation. A valid first diff may be a source-local contract/scaffold for a compute-backed gravity height producer that runs beside the camera path and does not rewire consumers yet.
- A previous supervised implementation attempt already performed source-only inspection and left no diff. Do not repeat that pattern. Reuse the accepted checkpoint and inspect only what you need to place the smallest safe scaffold.
- A good minimal patch shape is a disabled/parallel gravity heightfield compatibility producer or contract in the rendering layer, plus any tiny supporting compute-shader stub needed to express the intended data path. Preserve legacy texture names and do not rewire consumers in this pass.
- Before stopping, check `git status --short`. If it still shows no source diff, continue implementing unless you have found a specific source contradiction or missing dependency that makes even the compatibility scaffold unsafe.
- Do not attempt the full fog/froxel migration in this pass unless the compatibility producer and contract evidence are already in place.
- Before stopping, check `git status --short`. If it still shows no source diff, return a concrete source-level blocker naming the files inspected and the exact contradiction or missing dependency.
- Keep the change reviewable. Update or add source-local notes/tests if the repo has an established place for them.
- If the source contradicts the checkpoint, stop and explain the contradiction instead of tower-building.
'''

View file

@ -0,0 +1,794 @@
use crate::cultnet::EpiphanyFreshnessSurface;
use crate::cultnet::EpiphanyGraphFreshnessStatus;
use crate::cultnet::EpiphanyInvalidationStatus;
use crate::cultnet::EpiphanyJobKind;
use crate::cultnet::EpiphanyJobStatus;
use crate::cultnet::EpiphanyJobView;
use crate::cultnet::EpiphanyReorientAction;
use crate::cultnet::EpiphanyReorientCheckpointStatus;
use crate::cultnet::EpiphanyReorientDecision;
use crate::cultnet::EpiphanyReorientFreshnessStatus;
use crate::cultnet::EpiphanyReorientPressureLevel;
use crate::cultnet::EpiphanyReorientReason;
use crate::cultnet::EpiphanyReorientStateStatus;
use crate::cultnet::EpiphanyRetrievalFreshnessStatus;
use crate::cultnet::EpiphanySurfaceSource;
use codex_app_server_protocol::ThreadEpiphanyContextParams;
use codex_app_server_protocol::ThreadEpiphanyDistillParams;
use codex_app_server_protocol::ThreadEpiphanyFreshnessResponse;
use codex_app_server_protocol::ThreadEpiphanyFreshnessSource;
use codex_app_server_protocol::ThreadEpiphanyGraphFreshness;
use codex_app_server_protocol::ThreadEpiphanyGraphFreshnessStatus;
use codex_app_server_protocol::ThreadEpiphanyGraphQuery;
use codex_app_server_protocol::ThreadEpiphanyGraphQueryDirection;
use codex_app_server_protocol::ThreadEpiphanyGraphQueryKind;
use codex_app_server_protocol::ThreadEpiphanyInvalidationInput;
use codex_app_server_protocol::ThreadEpiphanyInvalidationStatus;
use codex_app_server_protocol::ThreadEpiphanyJob;
use codex_app_server_protocol::ThreadEpiphanyJobKind;
use codex_app_server_protocol::ThreadEpiphanyJobStatus;
use codex_app_server_protocol::ThreadEpiphanyPressure;
use codex_app_server_protocol::ThreadEpiphanyPressureBasis;
use codex_app_server_protocol::ThreadEpiphanyPressureLevel;
use codex_app_server_protocol::ThreadEpiphanyPressureStatus;
use codex_app_server_protocol::ThreadEpiphanyReorientAction;
use codex_app_server_protocol::ThreadEpiphanyReorientCheckpointStatus;
use codex_app_server_protocol::ThreadEpiphanyReorientDecision;
use codex_app_server_protocol::ThreadEpiphanyReorientFinding;
use codex_app_server_protocol::ThreadEpiphanyReorientReason;
use codex_app_server_protocol::ThreadEpiphanyReorientResultStatus;
use codex_app_server_protocol::ThreadEpiphanyReorientSource;
use codex_app_server_protocol::ThreadEpiphanyReorientStateStatus;
use codex_app_server_protocol::ThreadEpiphanyReorientWorkerLaunchDocument;
use codex_app_server_protocol::ThreadEpiphanyRetrievalFreshness;
use codex_app_server_protocol::ThreadEpiphanyRetrievalFreshnessStatus;
use codex_app_server_protocol::ThreadEpiphanyRoleFinding;
use codex_app_server_protocol::ThreadEpiphanyRoleId;
use codex_app_server_protocol::ThreadEpiphanyRoleResultStatus;
use codex_app_server_protocol::ThreadEpiphanyRoleSelfPersistenceReview;
use codex_app_server_protocol::ThreadEpiphanyRoleSelfPersistenceStatus;
use codex_app_server_protocol::ThreadEpiphanyRoleWorkerLaunchDocument;
use codex_app_server_protocol::ThreadEpiphanyRolesSource;
use codex_app_server_protocol::ThreadEpiphanyStateUpdatedField;
use codex_app_server_protocol::ThreadEpiphanyStateUpdatedNotification;
use codex_app_server_protocol::ThreadEpiphanyStateUpdatedSource;
use codex_app_server_protocol::ThreadEpiphanyUpdatePatch;
use codex_app_server_protocol::ThreadEpiphanyViewLens;
use codex_app_server_protocol::ThreadEpiphanyWorkerLaunchDocument;
use epiphany_core::EpiphanyContextParams;
use epiphany_core::EpiphanyCoordinatorRoleResultStatus;
use epiphany_core::EpiphanyCrrcResultStatus;
use epiphany_core::EpiphanyDistillInput;
use epiphany_core::EpiphanyGraphQuery;
use epiphany_core::EpiphanyGraphQueryDirection;
use epiphany_core::EpiphanyGraphQueryKind;
use epiphany_core::EpiphanyPressure;
use epiphany_core::EpiphanyPressureBasis;
use epiphany_core::EpiphanyPressureLevel;
use epiphany_core::EpiphanyPressureStatus;
use epiphany_core::EpiphanyReorientFindingInterpretation;
use epiphany_core::EpiphanyReorientWorkerLaunchDocument;
use epiphany_core::EpiphanyRoleFindingInterpretation;
use epiphany_core::EpiphanyRoleResultRoleId;
use epiphany_core::EpiphanyRoleSelfPersistenceReview;
use epiphany_core::EpiphanyRoleSelfPersistenceStatus;
use epiphany_core::EpiphanyRoleStatePatchDocument;
use epiphany_core::EpiphanyRoleWorkerLaunchDocument;
use epiphany_core::EpiphanyStateUpdatedField;
use epiphany_core::EpiphanyViewLens;
use epiphany_core::EpiphanyWorkerLaunchDocument;
use epiphany_core::default_epiphany_view_lenses;
use epiphany_core::epiphany_view_needs_jobs;
use epiphany_core::epiphany_view_needs_pressure;
use epiphany_core::epiphany_view_needs_reorientation_inputs;
use epiphany_core::epiphany_view_needs_runtime_store;
use epiphany_state_model::EpiphanyThreadState;
pub fn default_core_epiphany_view_lenses() -> Vec<EpiphanyViewLens> {
default_epiphany_view_lenses()
}
pub fn protocol_view_lenses_to_core(lenses: Vec<ThreadEpiphanyViewLens>) -> Vec<EpiphanyViewLens> {
lenses.into_iter().map(protocol_view_lens_to_core).collect()
}
pub fn protocol_view_lens_from_core(lens: EpiphanyViewLens) -> ThreadEpiphanyViewLens {
match lens {
EpiphanyViewLens::Scene => ThreadEpiphanyViewLens::Scene,
EpiphanyViewLens::Jobs => ThreadEpiphanyViewLens::Jobs,
EpiphanyViewLens::Roles => ThreadEpiphanyViewLens::Roles,
EpiphanyViewLens::Planning => ThreadEpiphanyViewLens::Planning,
EpiphanyViewLens::Pressure => ThreadEpiphanyViewLens::Pressure,
EpiphanyViewLens::Reorient => ThreadEpiphanyViewLens::Reorient,
EpiphanyViewLens::Crrc => ThreadEpiphanyViewLens::Crrc,
EpiphanyViewLens::Coordinator => ThreadEpiphanyViewLens::Coordinator,
}
}
pub fn core_epiphany_view_needs_jobs(lenses: &[EpiphanyViewLens]) -> bool {
epiphany_view_needs_jobs(lenses)
}
pub fn core_epiphany_view_needs_reorientation_inputs(lenses: &[EpiphanyViewLens]) -> bool {
epiphany_view_needs_reorientation_inputs(lenses)
}
pub fn core_epiphany_view_needs_pressure(lenses: &[EpiphanyViewLens]) -> bool {
epiphany_view_needs_pressure(lenses)
}
pub fn core_epiphany_view_needs_runtime_store(lenses: &[EpiphanyViewLens]) -> bool {
epiphany_view_needs_runtime_store(lenses)
}
pub fn protocol_context_params_to_core(
params: &ThreadEpiphanyContextParams,
) -> EpiphanyContextParams {
EpiphanyContextParams {
graph_node_ids: params.graph_node_ids.clone(),
graph_edge_ids: params.graph_edge_ids.clone(),
observation_ids: params.observation_ids.clone(),
evidence_ids: params.evidence_ids.clone(),
include_active_frontier: params.include_active_frontier,
include_linked_evidence: params.include_linked_evidence,
}
}
pub fn protocol_distill_params_to_core(
params: ThreadEpiphanyDistillParams,
) -> EpiphanyDistillInput {
let ThreadEpiphanyDistillParams {
source_kind,
status,
text,
subject,
evidence_kind,
code_refs,
..
} = params;
EpiphanyDistillInput {
source_kind,
status,
text,
subject,
evidence_kind,
code_refs,
}
}
pub fn protocol_graph_query_to_core(query: &ThreadEpiphanyGraphQuery) -> EpiphanyGraphQuery {
EpiphanyGraphQuery {
kind: protocol_graph_query_kind_to_core(query.kind),
node_ids: query.node_ids.clone(),
edge_ids: query.edge_ids.clone(),
paths: query.paths.clone(),
symbols: query.symbols.clone(),
edge_kinds: query.edge_kinds.clone(),
direction: query.direction.map(protocol_graph_query_direction_to_core),
depth: query.depth,
include_links: query.include_links,
}
}
pub fn protocol_worker_launch_document_to_core(
document: ThreadEpiphanyWorkerLaunchDocument,
) -> EpiphanyWorkerLaunchDocument {
match document {
ThreadEpiphanyWorkerLaunchDocument::Role(document) => EpiphanyWorkerLaunchDocument::Role(
protocol_role_worker_launch_document_to_core(document),
),
ThreadEpiphanyWorkerLaunchDocument::Reorient(document) => {
EpiphanyWorkerLaunchDocument::Reorient(
protocol_reorient_worker_launch_document_to_core(document),
)
}
}
}
pub fn protocol_update_patch_to_core(
patch: &ThreadEpiphanyUpdatePatch,
) -> EpiphanyRoleStatePatchDocument {
EpiphanyRoleStatePatchDocument {
objective: patch.objective.clone(),
active_subgoal_id: patch.active_subgoal_id.clone(),
subgoals: patch.subgoals.clone(),
invariants: patch.invariants.clone(),
graphs: patch.graphs.clone(),
graph_frontier: patch.graph_frontier.clone(),
graph_checkpoint: patch.graph_checkpoint.clone(),
scratch: patch.scratch.clone(),
investigation_checkpoint: patch.investigation_checkpoint.clone(),
job_bindings: patch.job_bindings.clone(),
acceptance_receipts: patch.acceptance_receipts.clone(),
runtime_links: patch.runtime_links.clone(),
observations: patch.observations.clone(),
evidence: patch.evidence.clone(),
churn: patch.churn.clone(),
mode: patch.mode.clone(),
planning: patch.planning.clone(),
}
}
pub fn protocol_patch_from_core(
patch: EpiphanyRoleStatePatchDocument,
) -> ThreadEpiphanyUpdatePatch {
ThreadEpiphanyUpdatePatch {
objective: patch.objective,
active_subgoal_id: patch.active_subgoal_id,
subgoals: patch.subgoals,
invariants: patch.invariants,
graphs: patch.graphs,
graph_frontier: patch.graph_frontier,
graph_checkpoint: patch.graph_checkpoint,
scratch: patch.scratch,
investigation_checkpoint: patch.investigation_checkpoint,
job_bindings: patch.job_bindings,
acceptance_receipts: patch.acceptance_receipts,
runtime_links: patch.runtime_links,
observations: patch.observations,
evidence: patch.evidence,
churn: patch.churn,
mode: patch.mode,
planning: patch.planning,
}
}
pub fn protocol_role_id_to_core(role_id: ThreadEpiphanyRoleId) -> EpiphanyRoleResultRoleId {
match role_id {
ThreadEpiphanyRoleId::Implementation => EpiphanyRoleResultRoleId::Implementation,
ThreadEpiphanyRoleId::Imagination => EpiphanyRoleResultRoleId::Imagination,
ThreadEpiphanyRoleId::Modeling => EpiphanyRoleResultRoleId::Modeling,
ThreadEpiphanyRoleId::Verification => EpiphanyRoleResultRoleId::Verification,
ThreadEpiphanyRoleId::Reorientation => EpiphanyRoleResultRoleId::Reorientation,
}
}
pub fn protocol_role_id_from_core(role_id: EpiphanyRoleResultRoleId) -> ThreadEpiphanyRoleId {
match role_id {
EpiphanyRoleResultRoleId::Implementation => ThreadEpiphanyRoleId::Implementation,
EpiphanyRoleResultRoleId::Imagination => ThreadEpiphanyRoleId::Imagination,
EpiphanyRoleResultRoleId::Modeling => ThreadEpiphanyRoleId::Modeling,
EpiphanyRoleResultRoleId::Verification => ThreadEpiphanyRoleId::Verification,
EpiphanyRoleResultRoleId::Reorientation => ThreadEpiphanyRoleId::Reorientation,
}
}
pub fn protocol_reorient_finding(
finding: EpiphanyReorientFindingInterpretation,
) -> ThreadEpiphanyReorientFinding {
ThreadEpiphanyReorientFinding {
mode: finding.mode,
summary: finding.summary,
next_safe_move: finding.next_safe_move,
checkpoint_still_valid: finding.checkpoint_still_valid,
files_inspected: finding.files_inspected,
frontier_node_ids: finding.frontier_node_ids,
evidence_ids: finding.evidence_ids,
artifact_refs: finding.artifact_refs,
runtime_result_id: finding.runtime_result_id,
runtime_job_id: finding.runtime_job_id,
job_error: finding.job_error,
item_error: finding.item_error,
}
}
pub fn protocol_role_finding(
role_id: ThreadEpiphanyRoleId,
finding: EpiphanyRoleFindingInterpretation,
) -> ThreadEpiphanyRoleFinding {
let state_patch = finding.state_patch.map(protocol_patch_from_core);
ThreadEpiphanyRoleFinding {
role_id,
verdict: finding.verdict,
summary: finding.summary,
next_safe_move: finding.next_safe_move,
checkpoint_summary: finding.checkpoint_summary,
scratch_summary: finding.scratch_summary,
files_inspected: finding.files_inspected,
frontier_node_ids: finding.frontier_node_ids,
evidence_ids: finding.evidence_ids,
artifact_refs: finding.artifact_refs,
runtime_result_id: finding.runtime_result_id,
runtime_job_id: finding.runtime_job_id,
open_questions: finding.open_questions,
evidence_gaps: finding.evidence_gaps,
risks: finding.risks,
state_patch,
self_patch: finding.self_patch.map(|patch| {
serde_json::to_value(patch)
.expect("AgentSelfPatch is a serializable protocol projection")
}),
self_persistence: finding
.self_persistence
.map(protocol_self_persistence_review),
job_error: finding.job_error,
item_error: finding.item_error,
}
}
pub fn protocol_state_updated_notification(
thread_id: String,
source: ThreadEpiphanyStateUpdatedSource,
revision: u64,
changed_fields: Vec<EpiphanyStateUpdatedField>,
epiphany_state: EpiphanyThreadState,
) -> ThreadEpiphanyStateUpdatedNotification {
ThreadEpiphanyStateUpdatedNotification {
thread_id,
source,
revision,
changed_fields: protocol_state_updated_fields(changed_fields),
epiphany_state,
}
}
pub fn protocol_state_updated_fields(
fields: Vec<EpiphanyStateUpdatedField>,
) -> Vec<ThreadEpiphanyStateUpdatedField> {
fields
.into_iter()
.map(protocol_state_updated_field)
.collect()
}
pub fn protocol_state_updated_field(
field: EpiphanyStateUpdatedField,
) -> ThreadEpiphanyStateUpdatedField {
match field {
EpiphanyStateUpdatedField::Objective => ThreadEpiphanyStateUpdatedField::Objective,
EpiphanyStateUpdatedField::ActiveSubgoalId => {
ThreadEpiphanyStateUpdatedField::ActiveSubgoalId
}
EpiphanyStateUpdatedField::Subgoals => ThreadEpiphanyStateUpdatedField::Subgoals,
EpiphanyStateUpdatedField::Invariants => ThreadEpiphanyStateUpdatedField::Invariants,
EpiphanyStateUpdatedField::Graphs => ThreadEpiphanyStateUpdatedField::Graphs,
EpiphanyStateUpdatedField::GraphFrontier => ThreadEpiphanyStateUpdatedField::GraphFrontier,
EpiphanyStateUpdatedField::GraphCheckpoint => {
ThreadEpiphanyStateUpdatedField::GraphCheckpoint
}
EpiphanyStateUpdatedField::Scratch => ThreadEpiphanyStateUpdatedField::Scratch,
EpiphanyStateUpdatedField::InvestigationCheckpoint => {
ThreadEpiphanyStateUpdatedField::InvestigationCheckpoint
}
EpiphanyStateUpdatedField::JobBindings => ThreadEpiphanyStateUpdatedField::JobBindings,
EpiphanyStateUpdatedField::AcceptanceReceipts => {
ThreadEpiphanyStateUpdatedField::AcceptanceReceipts
}
EpiphanyStateUpdatedField::RuntimeLinks => ThreadEpiphanyStateUpdatedField::RuntimeLinks,
EpiphanyStateUpdatedField::Observations => ThreadEpiphanyStateUpdatedField::Observations,
EpiphanyStateUpdatedField::Evidence => ThreadEpiphanyStateUpdatedField::Evidence,
EpiphanyStateUpdatedField::Churn => ThreadEpiphanyStateUpdatedField::Churn,
EpiphanyStateUpdatedField::Mode => ThreadEpiphanyStateUpdatedField::Mode,
EpiphanyStateUpdatedField::Planning => ThreadEpiphanyStateUpdatedField::Planning,
}
}
pub fn protocol_freshness_response_from_surface(
surface: EpiphanyFreshnessSurface,
) -> ThreadEpiphanyFreshnessResponse {
ThreadEpiphanyFreshnessResponse {
thread_id: surface.thread_id,
source: match surface.source {
EpiphanySurfaceSource::Stored => ThreadEpiphanyFreshnessSource::Stored,
EpiphanySurfaceSource::Live => ThreadEpiphanyFreshnessSource::Live,
},
state_revision: surface.state_revision,
retrieval: ThreadEpiphanyRetrievalFreshness {
status: match surface.retrieval.status {
EpiphanyRetrievalFreshnessStatus::Missing => {
ThreadEpiphanyRetrievalFreshnessStatus::Missing
}
EpiphanyRetrievalFreshnessStatus::Ready => {
ThreadEpiphanyRetrievalFreshnessStatus::Ready
}
EpiphanyRetrievalFreshnessStatus::Stale => {
ThreadEpiphanyRetrievalFreshnessStatus::Stale
}
EpiphanyRetrievalFreshnessStatus::Indexing => {
ThreadEpiphanyRetrievalFreshnessStatus::Indexing
}
EpiphanyRetrievalFreshnessStatus::Unavailable => {
ThreadEpiphanyRetrievalFreshnessStatus::Unavailable
}
},
semantic_available: surface.retrieval.semantic_available,
last_indexed_at_unix_seconds: surface.retrieval.last_indexed_at_unix_seconds,
indexed_file_count: surface.retrieval.indexed_file_count,
indexed_chunk_count: surface.retrieval.indexed_chunk_count,
dirty_paths: surface.retrieval.dirty_paths,
note: surface.retrieval.note,
},
graph: ThreadEpiphanyGraphFreshness {
status: match surface.graph.status {
EpiphanyGraphFreshnessStatus::Missing => {
ThreadEpiphanyGraphFreshnessStatus::Missing
}
EpiphanyGraphFreshnessStatus::Ready => ThreadEpiphanyGraphFreshnessStatus::Ready,
EpiphanyGraphFreshnessStatus::Stale => ThreadEpiphanyGraphFreshnessStatus::Stale,
},
graph_freshness: surface.graph.graph_freshness,
checkpoint_id: surface.graph.checkpoint_id,
dirty_path_count: surface.graph.dirty_path_count,
dirty_paths: surface.graph.dirty_paths,
open_question_count: surface.graph.open_question_count,
open_gap_count: surface.graph.open_gap_count,
note: surface.graph.note,
},
watcher: ThreadEpiphanyInvalidationInput {
status: match surface.watcher.status {
EpiphanyInvalidationStatus::Unavailable => {
ThreadEpiphanyInvalidationStatus::Unavailable
}
EpiphanyInvalidationStatus::Clean => ThreadEpiphanyInvalidationStatus::Clean,
EpiphanyInvalidationStatus::Changed => ThreadEpiphanyInvalidationStatus::Changed,
},
watched_root: surface.watcher.watched_root,
observed_at_unix_seconds: surface.watcher.observed_at_unix_seconds,
changed_path_count: surface.watcher.changed_path_count,
changed_paths: surface.watcher.changed_paths,
graph_node_ids: surface.watcher.graph_node_ids,
active_frontier_node_ids: surface.watcher.active_frontier_node_ids,
note: surface.watcher.note,
},
}
}
pub fn protocol_job_from_surface(
job: EpiphanyJobView,
launcher_job_id: Option<String>,
backend_job_id_override: Option<String>,
) -> ThreadEpiphanyJob {
ThreadEpiphanyJob {
id: job.id,
kind: match job.kind {
EpiphanyJobKind::Indexing => ThreadEpiphanyJobKind::Indexing,
EpiphanyJobKind::Remap => ThreadEpiphanyJobKind::Remap,
EpiphanyJobKind::Verification => ThreadEpiphanyJobKind::Verification,
EpiphanyJobKind::Specialist => ThreadEpiphanyJobKind::Specialist,
},
scope: job.scope,
owner_role: job.owner_role,
launcher_job_id,
authority_scope: job.authority_scope,
backend_job_id: backend_job_id_override.or(job.runtime_job_id),
status: match job.status {
EpiphanyJobStatus::Idle => ThreadEpiphanyJobStatus::Idle,
EpiphanyJobStatus::Needed => ThreadEpiphanyJobStatus::Needed,
EpiphanyJobStatus::Pending => ThreadEpiphanyJobStatus::Pending,
EpiphanyJobStatus::Running => ThreadEpiphanyJobStatus::Running,
EpiphanyJobStatus::Completed => ThreadEpiphanyJobStatus::Completed,
EpiphanyJobStatus::Failed => ThreadEpiphanyJobStatus::Failed,
EpiphanyJobStatus::Cancelled => ThreadEpiphanyJobStatus::Cancelled,
EpiphanyJobStatus::Blocked => ThreadEpiphanyJobStatus::Blocked,
EpiphanyJobStatus::Unavailable => ThreadEpiphanyJobStatus::Unavailable,
},
items_processed: job.items_processed,
items_total: job.items_total,
progress_note: job.progress_note,
last_checkpoint_at_unix_seconds: job.last_checkpoint_at_unix_seconds,
blocking_reason: job.blocking_reason,
active_thread_ids: job.active_thread_ids,
linked_subgoal_ids: job.linked_subgoal_ids,
linked_graph_node_ids: job.linked_graph_node_ids,
}
}
pub fn protocol_reorient_state_status(
status: EpiphanyReorientStateStatus,
) -> ThreadEpiphanyReorientStateStatus {
match status {
EpiphanyReorientStateStatus::Missing => ThreadEpiphanyReorientStateStatus::Missing,
EpiphanyReorientStateStatus::Ready => ThreadEpiphanyReorientStateStatus::Ready,
}
}
pub fn protocol_reorient_source(source: EpiphanySurfaceSource) -> ThreadEpiphanyReorientSource {
match source {
EpiphanySurfaceSource::Stored => ThreadEpiphanyReorientSource::Stored,
EpiphanySurfaceSource::Live => ThreadEpiphanyReorientSource::Live,
}
}
pub fn protocol_roles_source(source: EpiphanySurfaceSource) -> ThreadEpiphanyRolesSource {
match source {
EpiphanySurfaceSource::Stored => ThreadEpiphanyRolesSource::Stored,
EpiphanySurfaceSource::Live => ThreadEpiphanyRolesSource::Live,
}
}
pub fn protocol_reorient_decision(
decision: EpiphanyReorientDecision,
) -> ThreadEpiphanyReorientDecision {
ThreadEpiphanyReorientDecision {
action: match decision.action {
EpiphanyReorientAction::Resume => ThreadEpiphanyReorientAction::Resume,
EpiphanyReorientAction::Regather => ThreadEpiphanyReorientAction::Regather,
},
checkpoint_status: match decision.checkpoint_status {
EpiphanyReorientCheckpointStatus::Missing => {
ThreadEpiphanyReorientCheckpointStatus::Missing
}
EpiphanyReorientCheckpointStatus::ResumeReady => {
ThreadEpiphanyReorientCheckpointStatus::ResumeReady
}
EpiphanyReorientCheckpointStatus::RegatherRequired => {
ThreadEpiphanyReorientCheckpointStatus::RegatherRequired
}
},
checkpoint_id: decision.checkpoint_id,
pressure_level: match decision.pressure_level {
EpiphanyReorientPressureLevel::Unknown => ThreadEpiphanyPressureLevel::Unknown,
EpiphanyReorientPressureLevel::Low => ThreadEpiphanyPressureLevel::Low,
EpiphanyReorientPressureLevel::Medium => ThreadEpiphanyPressureLevel::Elevated,
EpiphanyReorientPressureLevel::High => ThreadEpiphanyPressureLevel::High,
EpiphanyReorientPressureLevel::Critical => ThreadEpiphanyPressureLevel::Critical,
},
retrieval_status: protocol_reorient_retrieval_status(decision.retrieval_status),
graph_status: protocol_reorient_graph_status(decision.graph_status),
watcher_status: protocol_reorient_watcher_status(decision.watcher_status),
reasons: decision
.reasons
.into_iter()
.map(protocol_reorient_reason)
.collect(),
checkpoint_dirty_paths: decision.checkpoint_dirty_paths,
checkpoint_changed_paths: decision.checkpoint_changed_paths,
active_frontier_node_ids: decision.active_frontier_node_ids,
next_action: decision.next_action,
note: decision.note,
}
}
pub fn protocol_pressure_from_core(pressure: EpiphanyPressure) -> ThreadEpiphanyPressure {
ThreadEpiphanyPressure {
status: match pressure.status {
EpiphanyPressureStatus::Unknown => ThreadEpiphanyPressureStatus::Unknown,
EpiphanyPressureStatus::Ready => ThreadEpiphanyPressureStatus::Ready,
},
level: protocol_pressure_level(pressure.level),
basis: match pressure.basis {
EpiphanyPressureBasis::Unknown => ThreadEpiphanyPressureBasis::Unknown,
EpiphanyPressureBasis::AutoCompactLimit => {
ThreadEpiphanyPressureBasis::AutoCompactLimit
}
EpiphanyPressureBasis::ModelContextWindow => {
ThreadEpiphanyPressureBasis::ModelContextWindow
}
},
used_tokens: pressure.used_tokens,
model_context_window: pressure.model_context_window,
model_auto_compact_token_limit: pressure.model_auto_compact_token_limit,
remaining_tokens: pressure.remaining_tokens,
ratio_per_mille: pressure.ratio_per_mille,
should_prepare_compaction: pressure.should_prepare_compaction,
note: pressure.note,
}
}
pub fn protocol_pressure_level(level: EpiphanyPressureLevel) -> ThreadEpiphanyPressureLevel {
match level {
EpiphanyPressureLevel::Unknown => ThreadEpiphanyPressureLevel::Unknown,
EpiphanyPressureLevel::Low => ThreadEpiphanyPressureLevel::Low,
EpiphanyPressureLevel::Elevated => ThreadEpiphanyPressureLevel::Elevated,
EpiphanyPressureLevel::High => ThreadEpiphanyPressureLevel::High,
EpiphanyPressureLevel::Critical => ThreadEpiphanyPressureLevel::Critical,
}
}
pub fn protocol_role_result_status(
status: EpiphanyCoordinatorRoleResultStatus,
) -> ThreadEpiphanyRoleResultStatus {
match status {
EpiphanyCoordinatorRoleResultStatus::MissingState => {
ThreadEpiphanyRoleResultStatus::MissingState
}
EpiphanyCoordinatorRoleResultStatus::MissingBinding => {
ThreadEpiphanyRoleResultStatus::MissingBinding
}
EpiphanyCoordinatorRoleResultStatus::BackendUnavailable => {
ThreadEpiphanyRoleResultStatus::BackendUnavailable
}
EpiphanyCoordinatorRoleResultStatus::BackendMissing => {
ThreadEpiphanyRoleResultStatus::BackendMissing
}
EpiphanyCoordinatorRoleResultStatus::Pending => ThreadEpiphanyRoleResultStatus::Pending,
EpiphanyCoordinatorRoleResultStatus::Running => ThreadEpiphanyRoleResultStatus::Running,
EpiphanyCoordinatorRoleResultStatus::Completed => ThreadEpiphanyRoleResultStatus::Completed,
EpiphanyCoordinatorRoleResultStatus::Failed => ThreadEpiphanyRoleResultStatus::Failed,
EpiphanyCoordinatorRoleResultStatus::Cancelled => ThreadEpiphanyRoleResultStatus::Cancelled,
}
}
pub fn protocol_reorient_result_status(
status: EpiphanyCrrcResultStatus,
) -> ThreadEpiphanyReorientResultStatus {
match status {
EpiphanyCrrcResultStatus::MissingState => ThreadEpiphanyReorientResultStatus::MissingState,
EpiphanyCrrcResultStatus::MissingBinding => {
ThreadEpiphanyReorientResultStatus::MissingBinding
}
EpiphanyCrrcResultStatus::BackendUnavailable => {
ThreadEpiphanyReorientResultStatus::BackendUnavailable
}
EpiphanyCrrcResultStatus::BackendMissing => {
ThreadEpiphanyReorientResultStatus::BackendMissing
}
EpiphanyCrrcResultStatus::Pending => ThreadEpiphanyReorientResultStatus::Pending,
EpiphanyCrrcResultStatus::Running => ThreadEpiphanyReorientResultStatus::Running,
EpiphanyCrrcResultStatus::Completed => ThreadEpiphanyReorientResultStatus::Completed,
EpiphanyCrrcResultStatus::Failed => ThreadEpiphanyReorientResultStatus::Failed,
EpiphanyCrrcResultStatus::Cancelled => ThreadEpiphanyReorientResultStatus::Cancelled,
}
}
fn protocol_view_lens_to_core(lens: ThreadEpiphanyViewLens) -> EpiphanyViewLens {
match lens {
ThreadEpiphanyViewLens::Scene => EpiphanyViewLens::Scene,
ThreadEpiphanyViewLens::Jobs => EpiphanyViewLens::Jobs,
ThreadEpiphanyViewLens::Roles => EpiphanyViewLens::Roles,
ThreadEpiphanyViewLens::Planning => EpiphanyViewLens::Planning,
ThreadEpiphanyViewLens::Pressure => EpiphanyViewLens::Pressure,
ThreadEpiphanyViewLens::Reorient => EpiphanyViewLens::Reorient,
ThreadEpiphanyViewLens::Crrc => EpiphanyViewLens::Crrc,
ThreadEpiphanyViewLens::Coordinator => EpiphanyViewLens::Coordinator,
}
}
fn protocol_graph_query_kind_to_core(kind: ThreadEpiphanyGraphQueryKind) -> EpiphanyGraphQueryKind {
match kind {
ThreadEpiphanyGraphQueryKind::Node => EpiphanyGraphQueryKind::Node,
ThreadEpiphanyGraphQueryKind::Path => EpiphanyGraphQueryKind::Path,
ThreadEpiphanyGraphQueryKind::FrontierNeighborhood => {
EpiphanyGraphQueryKind::FrontierNeighborhood
}
ThreadEpiphanyGraphQueryKind::Neighbors => EpiphanyGraphQueryKind::Neighbors,
}
}
fn protocol_graph_query_direction_to_core(
direction: ThreadEpiphanyGraphQueryDirection,
) -> EpiphanyGraphQueryDirection {
match direction {
ThreadEpiphanyGraphQueryDirection::Incoming => EpiphanyGraphQueryDirection::Incoming,
ThreadEpiphanyGraphQueryDirection::Outgoing => EpiphanyGraphQueryDirection::Outgoing,
ThreadEpiphanyGraphQueryDirection::Both => EpiphanyGraphQueryDirection::Both,
}
}
fn protocol_role_worker_launch_document_to_core(
document: ThreadEpiphanyRoleWorkerLaunchDocument,
) -> EpiphanyRoleWorkerLaunchDocument {
EpiphanyRoleWorkerLaunchDocument {
thread_id: document.thread_id,
role_id: document.role_id,
state_revision: document.state_revision,
objective: document.objective,
active_subgoal_id: document.active_subgoal_id,
active_subgoals: document.active_subgoals,
active_graph_node_ids: document.active_graph_node_ids,
investigation_checkpoint: document.investigation_checkpoint,
scratch: document.scratch,
invariants: document.invariants,
graphs: document.graphs,
recent_evidence: document.recent_evidence,
recent_observations: document.recent_observations,
graph_frontier: document.graph_frontier,
graph_checkpoint: document.graph_checkpoint,
planning: document.planning,
churn: document.churn,
}
}
fn protocol_reorient_worker_launch_document_to_core(
document: ThreadEpiphanyReorientWorkerLaunchDocument,
) -> EpiphanyReorientWorkerLaunchDocument {
EpiphanyReorientWorkerLaunchDocument {
thread_id: document.thread_id,
mode: document.mode,
checkpoint_id: document.checkpoint_id,
checkpoint_kind: document.checkpoint_kind,
checkpoint_disposition: document.checkpoint_disposition,
checkpoint_focus: document.checkpoint_focus,
checkpoint_summary: document.checkpoint_summary,
checkpoint_next_action: document.checkpoint_next_action,
checkpoint_open_questions: document.checkpoint_open_questions,
checkpoint_evidence_ids: document.checkpoint_evidence_ids,
checkpoint_code_refs: document.checkpoint_code_refs,
decision_reasons: document.decision_reasons,
decision_note: document.decision_note,
pressure_level: document.pressure_level,
retrieval_status: document.retrieval_status,
graph_status: document.graph_status,
watcher_status: document.watcher_status,
checkpoint_dirty_paths: document.checkpoint_dirty_paths,
checkpoint_changed_paths: document.checkpoint_changed_paths,
scratch: document.scratch,
graphs: document.graphs,
recent_evidence: document.recent_evidence,
recent_observations: document.recent_observations,
active_frontier_node_ids: document.active_frontier_node_ids,
linked_subgoal_ids: document.linked_subgoal_ids,
linked_graph_node_ids: document.linked_graph_node_ids,
}
}
fn protocol_reorient_retrieval_status(
status: EpiphanyReorientFreshnessStatus,
) -> ThreadEpiphanyRetrievalFreshnessStatus {
match status {
EpiphanyReorientFreshnessStatus::Unknown => ThreadEpiphanyRetrievalFreshnessStatus::Missing,
EpiphanyReorientFreshnessStatus::Clean => ThreadEpiphanyRetrievalFreshnessStatus::Ready,
EpiphanyReorientFreshnessStatus::Dirty => ThreadEpiphanyRetrievalFreshnessStatus::Indexing,
EpiphanyReorientFreshnessStatus::Stale | EpiphanyReorientFreshnessStatus::Changed => {
ThreadEpiphanyRetrievalFreshnessStatus::Stale
}
}
}
fn protocol_reorient_graph_status(
status: EpiphanyReorientFreshnessStatus,
) -> ThreadEpiphanyGraphFreshnessStatus {
match status {
EpiphanyReorientFreshnessStatus::Unknown => ThreadEpiphanyGraphFreshnessStatus::Missing,
EpiphanyReorientFreshnessStatus::Clean => ThreadEpiphanyGraphFreshnessStatus::Ready,
EpiphanyReorientFreshnessStatus::Dirty
| EpiphanyReorientFreshnessStatus::Stale
| EpiphanyReorientFreshnessStatus::Changed => ThreadEpiphanyGraphFreshnessStatus::Stale,
}
}
fn protocol_reorient_watcher_status(
status: EpiphanyReorientFreshnessStatus,
) -> ThreadEpiphanyInvalidationStatus {
match status {
EpiphanyReorientFreshnessStatus::Unknown => ThreadEpiphanyInvalidationStatus::Unavailable,
EpiphanyReorientFreshnessStatus::Clean => ThreadEpiphanyInvalidationStatus::Clean,
EpiphanyReorientFreshnessStatus::Dirty
| EpiphanyReorientFreshnessStatus::Stale
| EpiphanyReorientFreshnessStatus::Changed => ThreadEpiphanyInvalidationStatus::Changed,
}
}
fn protocol_reorient_reason(reason: EpiphanyReorientReason) -> ThreadEpiphanyReorientReason {
match reason {
EpiphanyReorientReason::MissingState => ThreadEpiphanyReorientReason::MissingState,
EpiphanyReorientReason::MissingCheckpoint => {
ThreadEpiphanyReorientReason::MissingCheckpoint
}
EpiphanyReorientReason::CheckpointReady => ThreadEpiphanyReorientReason::CheckpointReady,
EpiphanyReorientReason::CheckpointRequestedRegather => {
ThreadEpiphanyReorientReason::CheckpointRequestedRegather
}
EpiphanyReorientReason::CheckpointPathsDirty => {
ThreadEpiphanyReorientReason::CheckpointPathsDirty
}
EpiphanyReorientReason::CheckpointPathsChanged => {
ThreadEpiphanyReorientReason::CheckpointPathsChanged
}
EpiphanyReorientReason::FrontierChanged => ThreadEpiphanyReorientReason::FrontierChanged,
EpiphanyReorientReason::UnanchoredCheckpointWhileStateStale => {
ThreadEpiphanyReorientReason::UnanchoredCheckpointWhileStateStale
}
}
}
fn protocol_self_persistence_review(
review: EpiphanyRoleSelfPersistenceReview,
) -> ThreadEpiphanyRoleSelfPersistenceReview {
ThreadEpiphanyRoleSelfPersistenceReview {
status: match review.status {
EpiphanyRoleSelfPersistenceStatus::Missing => {
ThreadEpiphanyRoleSelfPersistenceStatus::Missing
}
EpiphanyRoleSelfPersistenceStatus::Accepted => {
ThreadEpiphanyRoleSelfPersistenceStatus::Accepted
}
EpiphanyRoleSelfPersistenceStatus::Rejected => {
ThreadEpiphanyRoleSelfPersistenceStatus::Rejected
}
},
target_agent_id: review.target_agent_id,
target_path: review.target_path,
reasons: review.reasons,
}
}

View file

@ -1,18 +1,3 @@
use codex_app_server_protocol::ThreadEpiphanyGraphFreshness;
use codex_app_server_protocol::ThreadEpiphanyGraphFreshnessStatus;
use codex_app_server_protocol::ThreadEpiphanyInvalidationInput;
use codex_app_server_protocol::ThreadEpiphanyInvalidationStatus;
use codex_app_server_protocol::ThreadEpiphanyPressure;
use codex_app_server_protocol::ThreadEpiphanyPressureLevel;
use codex_app_server_protocol::ThreadEpiphanyReorientAction;
use codex_app_server_protocol::ThreadEpiphanyReorientCheckpointStatus;
use codex_app_server_protocol::ThreadEpiphanyReorientDecision;
use codex_app_server_protocol::ThreadEpiphanyReorientReason;
use codex_app_server_protocol::ThreadEpiphanyReorientStateStatus;
use codex_app_server_protocol::ThreadEpiphanyRetrievalFreshness;
use codex_app_server_protocol::ThreadEpiphanyRetrievalFreshnessStatus;
use codex_protocol::protocol::EpiphanyRetrievalState;
use codex_protocol::protocol::EpiphanyThreadState;
use epiphany_core::EpiphanyFreshnessInput;
use epiphany_core::EpiphanyFreshnessView;
use epiphany_core::EpiphanyFreshnessWatcherInput;
@ -20,17 +5,19 @@ use epiphany_core::EpiphanyGraphFreshness as CoreEpiphanyGraphFreshness;
use epiphany_core::EpiphanyGraphFreshnessStatus as CoreEpiphanyGraphFreshnessStatus;
use epiphany_core::EpiphanyInvalidationInput as CoreEpiphanyInvalidationInput;
use epiphany_core::EpiphanyInvalidationStatus as CoreEpiphanyInvalidationStatus;
use epiphany_core::EpiphanyReorientAction as CoreEpiphanyReorientAction;
use epiphany_core::EpiphanyReorientCheckpointStatus as CoreEpiphanyReorientCheckpointStatus;
use epiphany_core::EpiphanyPressure;
use epiphany_core::EpiphanyPressureLevel;
use epiphany_core::EpiphanyReorientDecision as CoreEpiphanyReorientDecision;
use epiphany_core::EpiphanyReorientFreshnessStatus as CoreEpiphanyReorientFreshnessStatus;
use epiphany_core::EpiphanyReorientInput;
use epiphany_core::EpiphanyReorientPressureLevel as CoreEpiphanyReorientPressureLevel;
use epiphany_core::EpiphanyReorientReason as CoreEpiphanyReorientReason;
use epiphany_core::EpiphanyReorientStateStatus as CoreEpiphanyReorientStateStatus;
use epiphany_core::EpiphanyRetrievalFreshness as CoreEpiphanyRetrievalFreshness;
use epiphany_core::EpiphanyRetrievalFreshnessStatus as CoreEpiphanyRetrievalFreshnessStatus;
use epiphany_core::derive_freshness;
use epiphany_core::recommend_reorientation;
use epiphany_state_model::EpiphanyRetrievalState;
use epiphany_state_model::EpiphanyThreadState;
use std::path::Path;
use std::path::PathBuf;
@ -41,129 +28,35 @@ pub struct EpiphanyFreshnessWatcherSnapshot<'a> {
pub changed_paths: &'a [PathBuf],
}
pub fn map_epiphany_freshness(
pub fn derive_epiphany_freshness_view(
state: Option<&EpiphanyThreadState>,
retrieval_override: Option<&EpiphanyRetrievalState>,
watcher_snapshot: Option<EpiphanyFreshnessWatcherSnapshot<'_>>,
) -> (
Option<u64>,
ThreadEpiphanyRetrievalFreshness,
ThreadEpiphanyGraphFreshness,
ThreadEpiphanyInvalidationInput,
) {
) -> EpiphanyFreshnessView {
let watcher = watcher_snapshot.map(|snapshot| EpiphanyFreshnessWatcherInput {
available: snapshot.available,
workspace_root: snapshot.workspace_root,
observed_at_unix_seconds: snapshot.observed_at_unix_seconds,
changed_paths: snapshot.changed_paths,
});
let freshness = derive_freshness(EpiphanyFreshnessInput {
derive_freshness(EpiphanyFreshnessInput {
state,
retrieval_override,
watcher,
});
map_core_epiphany_freshness(freshness)
})
}
fn map_core_epiphany_freshness(
freshness: EpiphanyFreshnessView,
) -> (
Option<u64>,
ThreadEpiphanyRetrievalFreshness,
ThreadEpiphanyGraphFreshness,
ThreadEpiphanyInvalidationInput,
) {
(
freshness.state_revision,
map_core_epiphany_retrieval_freshness(freshness.retrieval),
map_core_epiphany_graph_freshness(freshness.graph),
map_core_epiphany_invalidation_input(freshness.watcher),
)
}
fn map_core_epiphany_retrieval_freshness(
retrieval: CoreEpiphanyRetrievalFreshness,
) -> ThreadEpiphanyRetrievalFreshness {
ThreadEpiphanyRetrievalFreshness {
status: match retrieval.status {
CoreEpiphanyRetrievalFreshnessStatus::Missing => {
ThreadEpiphanyRetrievalFreshnessStatus::Missing
}
CoreEpiphanyRetrievalFreshnessStatus::Ready => {
ThreadEpiphanyRetrievalFreshnessStatus::Ready
}
CoreEpiphanyRetrievalFreshnessStatus::Stale => {
ThreadEpiphanyRetrievalFreshnessStatus::Stale
}
CoreEpiphanyRetrievalFreshnessStatus::Indexing => {
ThreadEpiphanyRetrievalFreshnessStatus::Indexing
}
CoreEpiphanyRetrievalFreshnessStatus::Unavailable => {
ThreadEpiphanyRetrievalFreshnessStatus::Unavailable
}
},
semantic_available: retrieval.semantic_available,
last_indexed_at_unix_seconds: retrieval.last_indexed_at_unix_seconds,
indexed_file_count: retrieval.indexed_file_count,
indexed_chunk_count: retrieval.indexed_chunk_count,
dirty_paths: retrieval.dirty_paths,
note: retrieval.note,
}
}
fn map_core_epiphany_graph_freshness(
graph: CoreEpiphanyGraphFreshness,
) -> ThreadEpiphanyGraphFreshness {
ThreadEpiphanyGraphFreshness {
status: match graph.status {
CoreEpiphanyGraphFreshnessStatus::Missing => {
ThreadEpiphanyGraphFreshnessStatus::Missing
}
CoreEpiphanyGraphFreshnessStatus::Ready => ThreadEpiphanyGraphFreshnessStatus::Ready,
CoreEpiphanyGraphFreshnessStatus::Stale => ThreadEpiphanyGraphFreshnessStatus::Stale,
},
graph_freshness: graph.graph_freshness,
checkpoint_id: graph.checkpoint_id,
dirty_path_count: graph.dirty_path_count,
dirty_paths: graph.dirty_paths,
open_question_count: graph.open_question_count,
open_gap_count: graph.open_gap_count,
note: graph.note,
}
}
fn map_core_epiphany_invalidation_input(
watcher: CoreEpiphanyInvalidationInput,
) -> ThreadEpiphanyInvalidationInput {
ThreadEpiphanyInvalidationInput {
status: match watcher.status {
CoreEpiphanyInvalidationStatus::Unavailable => {
ThreadEpiphanyInvalidationStatus::Unavailable
}
CoreEpiphanyInvalidationStatus::Clean => ThreadEpiphanyInvalidationStatus::Clean,
CoreEpiphanyInvalidationStatus::Changed => ThreadEpiphanyInvalidationStatus::Changed,
},
watched_root: watcher.watched_root,
observed_at_unix_seconds: watcher.observed_at_unix_seconds,
changed_path_count: watcher.changed_path_count,
changed_paths: watcher.changed_paths,
graph_node_ids: watcher.graph_node_ids,
active_frontier_node_ids: watcher.active_frontier_node_ids,
note: watcher.note,
}
}
pub fn map_epiphany_reorient(
pub fn derive_epiphany_reorient(
state: Option<&EpiphanyThreadState>,
pressure: &ThreadEpiphanyPressure,
retrieval: &ThreadEpiphanyRetrievalFreshness,
graph: &ThreadEpiphanyGraphFreshness,
watcher: &ThreadEpiphanyInvalidationInput,
pressure: &EpiphanyPressure,
retrieval: &CoreEpiphanyRetrievalFreshness,
graph: &CoreEpiphanyGraphFreshness,
watcher: &CoreEpiphanyInvalidationInput,
) -> (
ThreadEpiphanyReorientStateStatus,
ThreadEpiphanyReorientDecision,
CoreEpiphanyReorientStateStatus,
CoreEpiphanyReorientDecision,
) {
let (state_status, decision) = recommend_reorientation(EpiphanyReorientInput {
recommend_reorientation(EpiphanyReorientInput {
checkpoint: state.and_then(|state| state.investigation_checkpoint.as_ref()),
state_present: state.is_some(),
pressure_level: map_core_reorient_pressure_level(pressure.level),
@ -176,202 +69,53 @@ pub fn map_epiphany_reorient(
watcher_graph_node_ids: watcher.graph_node_ids.clone(),
active_frontier_node_ids: watcher.active_frontier_node_ids.clone(),
watched_root: watcher.watched_root.clone(),
});
(
map_protocol_reorient_state_status(state_status),
map_protocol_reorient_decision(decision),
)
})
}
fn map_core_reorient_pressure_level(
level: ThreadEpiphanyPressureLevel,
level: EpiphanyPressureLevel,
) -> CoreEpiphanyReorientPressureLevel {
match level {
ThreadEpiphanyPressureLevel::Unknown => CoreEpiphanyReorientPressureLevel::Unknown,
ThreadEpiphanyPressureLevel::Low => CoreEpiphanyReorientPressureLevel::Low,
ThreadEpiphanyPressureLevel::Elevated => CoreEpiphanyReorientPressureLevel::Medium,
ThreadEpiphanyPressureLevel::High => CoreEpiphanyReorientPressureLevel::High,
ThreadEpiphanyPressureLevel::Critical => CoreEpiphanyReorientPressureLevel::Critical,
EpiphanyPressureLevel::Unknown => CoreEpiphanyReorientPressureLevel::Unknown,
EpiphanyPressureLevel::Low => CoreEpiphanyReorientPressureLevel::Low,
EpiphanyPressureLevel::Elevated => CoreEpiphanyReorientPressureLevel::Medium,
EpiphanyPressureLevel::High => CoreEpiphanyReorientPressureLevel::High,
EpiphanyPressureLevel::Critical => CoreEpiphanyReorientPressureLevel::Critical,
}
}
fn map_core_reorient_retrieval_status(
status: ThreadEpiphanyRetrievalFreshnessStatus,
status: CoreEpiphanyRetrievalFreshnessStatus,
) -> CoreEpiphanyReorientFreshnessStatus {
match status {
ThreadEpiphanyRetrievalFreshnessStatus::Missing
| ThreadEpiphanyRetrievalFreshnessStatus::Unavailable => {
CoreEpiphanyRetrievalFreshnessStatus::Missing
| CoreEpiphanyRetrievalFreshnessStatus::Unavailable => {
CoreEpiphanyReorientFreshnessStatus::Unknown
}
ThreadEpiphanyRetrievalFreshnessStatus::Ready => CoreEpiphanyReorientFreshnessStatus::Clean,
ThreadEpiphanyRetrievalFreshnessStatus::Stale => CoreEpiphanyReorientFreshnessStatus::Stale,
ThreadEpiphanyRetrievalFreshnessStatus::Indexing => {
CoreEpiphanyRetrievalFreshnessStatus::Ready => CoreEpiphanyReorientFreshnessStatus::Clean,
CoreEpiphanyRetrievalFreshnessStatus::Stale => CoreEpiphanyReorientFreshnessStatus::Stale,
CoreEpiphanyRetrievalFreshnessStatus::Indexing => {
CoreEpiphanyReorientFreshnessStatus::Dirty
}
}
}
fn map_core_reorient_graph_status(
status: ThreadEpiphanyGraphFreshnessStatus,
status: CoreEpiphanyGraphFreshnessStatus,
) -> CoreEpiphanyReorientFreshnessStatus {
match status {
ThreadEpiphanyGraphFreshnessStatus::Missing => CoreEpiphanyReorientFreshnessStatus::Unknown,
ThreadEpiphanyGraphFreshnessStatus::Ready => CoreEpiphanyReorientFreshnessStatus::Clean,
ThreadEpiphanyGraphFreshnessStatus::Stale => CoreEpiphanyReorientFreshnessStatus::Stale,
CoreEpiphanyGraphFreshnessStatus::Missing => CoreEpiphanyReorientFreshnessStatus::Unknown,
CoreEpiphanyGraphFreshnessStatus::Ready => CoreEpiphanyReorientFreshnessStatus::Clean,
CoreEpiphanyGraphFreshnessStatus::Stale => CoreEpiphanyReorientFreshnessStatus::Stale,
}
}
fn map_core_reorient_watcher_status(
status: ThreadEpiphanyInvalidationStatus,
status: CoreEpiphanyInvalidationStatus,
) -> CoreEpiphanyReorientFreshnessStatus {
match status {
ThreadEpiphanyInvalidationStatus::Unavailable => {
CoreEpiphanyReorientFreshnessStatus::Unknown
}
ThreadEpiphanyInvalidationStatus::Clean => CoreEpiphanyReorientFreshnessStatus::Clean,
ThreadEpiphanyInvalidationStatus::Changed => CoreEpiphanyReorientFreshnessStatus::Changed,
}
}
fn map_protocol_reorient_state_status(
status: CoreEpiphanyReorientStateStatus,
) -> ThreadEpiphanyReorientStateStatus {
match status {
CoreEpiphanyReorientStateStatus::Missing => ThreadEpiphanyReorientStateStatus::Missing,
CoreEpiphanyReorientStateStatus::Ready => ThreadEpiphanyReorientStateStatus::Ready,
}
}
fn map_protocol_reorient_decision(
decision: epiphany_core::EpiphanyReorientDecision,
) -> ThreadEpiphanyReorientDecision {
ThreadEpiphanyReorientDecision {
action: map_protocol_reorient_action(decision.action),
checkpoint_status: map_protocol_reorient_checkpoint_status(decision.checkpoint_status),
checkpoint_id: decision.checkpoint_id,
pressure_level: map_protocol_reorient_pressure_level(decision.pressure_level),
retrieval_status: map_protocol_reorient_retrieval_status(decision.retrieval_status),
graph_status: map_protocol_reorient_graph_status(decision.graph_status),
watcher_status: map_protocol_reorient_watcher_status(decision.watcher_status),
reasons: decision
.reasons
.into_iter()
.map(map_protocol_reorient_reason)
.collect(),
checkpoint_dirty_paths: decision.checkpoint_dirty_paths,
checkpoint_changed_paths: decision.checkpoint_changed_paths,
active_frontier_node_ids: decision.active_frontier_node_ids,
next_action: decision.next_action,
note: decision.note,
}
}
fn map_protocol_reorient_action(
action: CoreEpiphanyReorientAction,
) -> ThreadEpiphanyReorientAction {
match action {
CoreEpiphanyReorientAction::Resume => ThreadEpiphanyReorientAction::Resume,
CoreEpiphanyReorientAction::Regather => ThreadEpiphanyReorientAction::Regather,
}
}
fn map_protocol_reorient_checkpoint_status(
status: CoreEpiphanyReorientCheckpointStatus,
) -> ThreadEpiphanyReorientCheckpointStatus {
match status {
CoreEpiphanyReorientCheckpointStatus::Missing => {
ThreadEpiphanyReorientCheckpointStatus::Missing
}
CoreEpiphanyReorientCheckpointStatus::ResumeReady => {
ThreadEpiphanyReorientCheckpointStatus::ResumeReady
}
CoreEpiphanyReorientCheckpointStatus::RegatherRequired => {
ThreadEpiphanyReorientCheckpointStatus::RegatherRequired
}
}
}
fn map_protocol_reorient_pressure_level(
level: CoreEpiphanyReorientPressureLevel,
) -> ThreadEpiphanyPressureLevel {
match level {
CoreEpiphanyReorientPressureLevel::Unknown => ThreadEpiphanyPressureLevel::Unknown,
CoreEpiphanyReorientPressureLevel::Low => ThreadEpiphanyPressureLevel::Low,
CoreEpiphanyReorientPressureLevel::Medium => ThreadEpiphanyPressureLevel::Elevated,
CoreEpiphanyReorientPressureLevel::High => ThreadEpiphanyPressureLevel::High,
CoreEpiphanyReorientPressureLevel::Critical => ThreadEpiphanyPressureLevel::Critical,
}
}
fn map_protocol_reorient_retrieval_status(
status: CoreEpiphanyReorientFreshnessStatus,
) -> ThreadEpiphanyRetrievalFreshnessStatus {
match status {
CoreEpiphanyReorientFreshnessStatus::Unknown => {
ThreadEpiphanyRetrievalFreshnessStatus::Missing
}
CoreEpiphanyReorientFreshnessStatus::Clean => ThreadEpiphanyRetrievalFreshnessStatus::Ready,
CoreEpiphanyReorientFreshnessStatus::Dirty => {
ThreadEpiphanyRetrievalFreshnessStatus::Indexing
}
CoreEpiphanyReorientFreshnessStatus::Stale
| CoreEpiphanyReorientFreshnessStatus::Changed => {
ThreadEpiphanyRetrievalFreshnessStatus::Stale
}
}
}
fn map_protocol_reorient_graph_status(
status: CoreEpiphanyReorientFreshnessStatus,
) -> ThreadEpiphanyGraphFreshnessStatus {
match status {
CoreEpiphanyReorientFreshnessStatus::Unknown => ThreadEpiphanyGraphFreshnessStatus::Missing,
CoreEpiphanyReorientFreshnessStatus::Clean => ThreadEpiphanyGraphFreshnessStatus::Ready,
CoreEpiphanyReorientFreshnessStatus::Dirty
| CoreEpiphanyReorientFreshnessStatus::Stale
| CoreEpiphanyReorientFreshnessStatus::Changed => ThreadEpiphanyGraphFreshnessStatus::Stale,
}
}
fn map_protocol_reorient_watcher_status(
status: CoreEpiphanyReorientFreshnessStatus,
) -> ThreadEpiphanyInvalidationStatus {
match status {
CoreEpiphanyReorientFreshnessStatus::Unknown => {
ThreadEpiphanyInvalidationStatus::Unavailable
}
CoreEpiphanyReorientFreshnessStatus::Clean => ThreadEpiphanyInvalidationStatus::Clean,
CoreEpiphanyReorientFreshnessStatus::Dirty
| CoreEpiphanyReorientFreshnessStatus::Stale
| CoreEpiphanyReorientFreshnessStatus::Changed => ThreadEpiphanyInvalidationStatus::Changed,
}
}
fn map_protocol_reorient_reason(
reason: CoreEpiphanyReorientReason,
) -> ThreadEpiphanyReorientReason {
match reason {
CoreEpiphanyReorientReason::MissingState => ThreadEpiphanyReorientReason::MissingState,
CoreEpiphanyReorientReason::MissingCheckpoint => {
ThreadEpiphanyReorientReason::MissingCheckpoint
}
CoreEpiphanyReorientReason::CheckpointReady => {
ThreadEpiphanyReorientReason::CheckpointReady
}
CoreEpiphanyReorientReason::CheckpointRequestedRegather => {
ThreadEpiphanyReorientReason::CheckpointRequestedRegather
}
CoreEpiphanyReorientReason::CheckpointPathsDirty => {
ThreadEpiphanyReorientReason::CheckpointPathsDirty
}
CoreEpiphanyReorientReason::CheckpointPathsChanged => {
ThreadEpiphanyReorientReason::CheckpointPathsChanged
}
CoreEpiphanyReorientReason::FrontierChanged => {
ThreadEpiphanyReorientReason::FrontierChanged
}
CoreEpiphanyReorientReason::UnanchoredCheckpointWhileStateStale => {
ThreadEpiphanyReorientReason::UnanchoredCheckpointWhileStateStale
}
CoreEpiphanyInvalidationStatus::Unavailable => CoreEpiphanyReorientFreshnessStatus::Unknown,
CoreEpiphanyInvalidationStatus::Clean => CoreEpiphanyReorientFreshnessStatus::Clean,
CoreEpiphanyInvalidationStatus::Changed => CoreEpiphanyReorientFreshnessStatus::Changed,
}
}

View file

@ -1,138 +1,23 @@
use codex_app_server_protocol::ThreadEpiphanyReorientFinding;
use codex_app_server_protocol::ThreadEpiphanyReorientResultStatus;
use codex_app_server_protocol::ThreadEpiphanyRoleFinding;
use codex_app_server_protocol::ThreadEpiphanyRoleId;
use codex_app_server_protocol::ThreadEpiphanyRoleResultStatus;
use codex_app_server_protocol::ThreadEpiphanyRoleSelfPersistenceReview;
use codex_app_server_protocol::ThreadEpiphanyRoleSelfPersistenceStatus;
use codex_app_server_protocol::ThreadEpiphanyUpdatePatch;
use epiphany_core::EpiphanyCoordinatorRoleResultStatus as CoreEpiphanyCoordinatorRoleResultStatus;
use epiphany_core::EpiphanyCrrcResultStatus as CoreEpiphanyCrrcResultStatus;
use epiphany_core::EpiphanyReorientFindingInterpretation;
use epiphany_core::EpiphanyRoleFindingInterpretation;
use epiphany_core::EpiphanyRoleResultRoleId;
use epiphany_core::EpiphanyRoleSelfPersistenceReview as CoreEpiphanyRoleSelfPersistenceReview;
use epiphany_core::EpiphanyRoleSelfPersistenceStatus as CoreEpiphanyRoleSelfPersistenceStatus;
use epiphany_core::EpiphanyRoleStatePatchDocument;
pub fn map_protocol_reorient_finding(
finding: EpiphanyReorientFindingInterpretation,
) -> ThreadEpiphanyReorientFinding {
ThreadEpiphanyReorientFinding {
mode: finding.mode,
summary: finding.summary,
next_safe_move: finding.next_safe_move,
checkpoint_still_valid: finding.checkpoint_still_valid,
files_inspected: finding.files_inspected,
frontier_node_ids: finding.frontier_node_ids,
evidence_ids: finding.evidence_ids,
artifact_refs: finding.artifact_refs,
runtime_result_id: finding.runtime_result_id,
runtime_job_id: finding.runtime_job_id,
job_error: finding.job_error,
item_error: finding.item_error,
}
}
pub fn map_protocol_role_finding(
role_id: ThreadEpiphanyRoleId,
finding: EpiphanyRoleFindingInterpretation,
) -> ThreadEpiphanyRoleFinding {
let state_patch = finding.state_patch.map(map_protocol_update_patch);
ThreadEpiphanyRoleFinding {
role_id,
verdict: finding.verdict,
summary: finding.summary,
next_safe_move: finding.next_safe_move,
checkpoint_summary: finding.checkpoint_summary,
scratch_summary: finding.scratch_summary,
files_inspected: finding.files_inspected,
frontier_node_ids: finding.frontier_node_ids,
evidence_ids: finding.evidence_ids,
artifact_refs: finding.artifact_refs,
runtime_result_id: finding.runtime_result_id,
runtime_job_id: finding.runtime_job_id,
open_questions: finding.open_questions,
evidence_gaps: finding.evidence_gaps,
risks: finding.risks,
state_patch,
self_patch: finding.self_patch.map(|patch| {
serde_json::to_value(patch)
.expect("AgentSelfPatch is a serializable protocol projection")
}),
self_persistence: finding
.self_persistence
.map(map_protocol_role_self_persistence_review),
job_error: finding.job_error,
item_error: finding.item_error,
}
}
fn map_protocol_update_patch(patch: EpiphanyRoleStatePatchDocument) -> ThreadEpiphanyUpdatePatch {
ThreadEpiphanyUpdatePatch {
objective: patch.objective,
active_subgoal_id: patch.active_subgoal_id,
subgoals: patch.subgoals,
invariants: patch.invariants,
graphs: patch.graphs,
graph_frontier: patch.graph_frontier,
graph_checkpoint: patch.graph_checkpoint,
scratch: patch.scratch,
investigation_checkpoint: patch.investigation_checkpoint,
job_bindings: patch.job_bindings,
acceptance_receipts: patch.acceptance_receipts,
runtime_links: patch.runtime_links,
observations: patch.observations,
evidence: patch.evidence,
churn: patch.churn,
mode: patch.mode,
planning: patch.planning,
}
}
pub fn map_core_role_result_role_id(
role_id: ThreadEpiphanyRoleId,
) -> EpiphanyRoleResultRoleId {
match role_id {
ThreadEpiphanyRoleId::Implementation => EpiphanyRoleResultRoleId::Implementation,
ThreadEpiphanyRoleId::Imagination => EpiphanyRoleResultRoleId::Imagination,
ThreadEpiphanyRoleId::Modeling => EpiphanyRoleResultRoleId::Modeling,
ThreadEpiphanyRoleId::Verification => EpiphanyRoleResultRoleId::Verification,
ThreadEpiphanyRoleId::Reorientation => EpiphanyRoleResultRoleId::Reorientation,
}
}
fn map_protocol_role_self_persistence_review(
review: CoreEpiphanyRoleSelfPersistenceReview,
) -> ThreadEpiphanyRoleSelfPersistenceReview {
ThreadEpiphanyRoleSelfPersistenceReview {
status: match review.status {
CoreEpiphanyRoleSelfPersistenceStatus::Missing => {
ThreadEpiphanyRoleSelfPersistenceStatus::Missing
}
CoreEpiphanyRoleSelfPersistenceStatus::Accepted => {
ThreadEpiphanyRoleSelfPersistenceStatus::Accepted
}
CoreEpiphanyRoleSelfPersistenceStatus::Rejected => {
ThreadEpiphanyRoleSelfPersistenceStatus::Rejected
}
},
target_agent_id: review.target_agent_id,
target_path: review.target_path,
reasons: review.reasons,
}
}
pub fn render_epiphany_role_result_note(
role_id: ThreadEpiphanyRoleId,
status: ThreadEpiphanyRoleResultStatus,
finding: Option<&ThreadEpiphanyRoleFinding>,
pub fn render_core_role_result_note(
role_id: EpiphanyRoleResultRoleId,
status: CoreEpiphanyCoordinatorRoleResultStatus,
finding: Option<&EpiphanyRoleFindingInterpretation>,
item_error: Option<&str>,
) -> String {
match status {
ThreadEpiphanyRoleResultStatus::Completed => {
CoreEpiphanyCoordinatorRoleResultStatus::Completed => {
if let Some(finding) = finding {
let next = finding.next_safe_move.as_deref().unwrap_or("not supplied");
let self_note =
render_epiphany_self_persistence_note(finding.self_persistence.as_ref())
render_core_self_persistence_note(finding.self_persistence.as_ref())
.map(|note| format!(" {note}"))
.unwrap_or_default();
if let Some(item_error) = finding.item_error.as_deref().or(item_error) {
@ -153,53 +38,53 @@ pub fn render_epiphany_role_result_note(
)
}
}
ThreadEpiphanyRoleResultStatus::Failed => item_error
CoreEpiphanyCoordinatorRoleResultStatus::Failed => item_error
.map(|error| format!("{:?} role specialist failed: {error}", role_id))
.unwrap_or_else(|| format!("{:?} role specialist failed.", role_id)),
ThreadEpiphanyRoleResultStatus::Cancelled => {
CoreEpiphanyCoordinatorRoleResultStatus::Cancelled => {
format!(
"{:?} role specialist was cancelled before producing a result.",
role_id
)
}
ThreadEpiphanyRoleResultStatus::Running => {
CoreEpiphanyCoordinatorRoleResultStatus::Running => {
format!("{:?} role specialist is still running.", role_id)
}
ThreadEpiphanyRoleResultStatus::Pending => {
CoreEpiphanyCoordinatorRoleResultStatus::Pending => {
format!(
"{:?} role specialist has not produced a result yet.",
role_id
)
}
ThreadEpiphanyRoleResultStatus::MissingState => {
CoreEpiphanyCoordinatorRoleResultStatus::MissingState => {
"No authoritative Epiphany state exists for this thread.".to_string()
}
ThreadEpiphanyRoleResultStatus::MissingBinding => {
CoreEpiphanyCoordinatorRoleResultStatus::MissingBinding => {
"No matching Epiphany role specialist binding exists.".to_string()
}
ThreadEpiphanyRoleResultStatus::BackendUnavailable => {
CoreEpiphanyCoordinatorRoleResultStatus::BackendUnavailable => {
"The bound runtime backend is unavailable.".to_string()
}
ThreadEpiphanyRoleResultStatus::BackendMissing => {
CoreEpiphanyCoordinatorRoleResultStatus::BackendMissing => {
"The bound runtime backend job or item is missing.".to_string()
}
}
}
fn render_epiphany_self_persistence_note(
review: Option<&ThreadEpiphanyRoleSelfPersistenceReview>,
fn render_core_self_persistence_note(
review: Option<&CoreEpiphanyRoleSelfPersistenceReview>,
) -> Option<String> {
let review = review?;
match review.status {
ThreadEpiphanyRoleSelfPersistenceStatus::Missing => None,
ThreadEpiphanyRoleSelfPersistenceStatus::Accepted => Some(format!(
CoreEpiphanyRoleSelfPersistenceStatus::Missing => None,
CoreEpiphanyRoleSelfPersistenceStatus::Accepted => Some(format!(
"Self persistence request is acceptable for {}.",
review
.target_agent_id
.as_deref()
.unwrap_or("the role memory file")
)),
ThreadEpiphanyRoleSelfPersistenceStatus::Rejected => {
CoreEpiphanyRoleSelfPersistenceStatus::Rejected => {
let reasons = if review.reasons.is_empty() {
"no reason recorded".to_string()
} else {
@ -210,13 +95,13 @@ fn render_epiphany_self_persistence_note(
}
}
pub fn render_epiphany_reorient_result_note(
status: ThreadEpiphanyReorientResultStatus,
finding: Option<&ThreadEpiphanyReorientFinding>,
pub fn render_core_reorient_result_note(
status: CoreEpiphanyCrrcResultStatus,
finding: Option<&EpiphanyReorientFindingInterpretation>,
item_error: Option<&str>,
) -> String {
match status {
ThreadEpiphanyReorientResultStatus::Completed => {
CoreEpiphanyCrrcResultStatus::Completed => {
if let Some(finding) = finding {
let next = finding.next_safe_move.as_deref().unwrap_or("not supplied");
format!("Reorientation worker completed. Next safe move: {next}")
@ -224,28 +109,28 @@ pub fn render_epiphany_reorient_result_note(
"Reorientation worker completed, but no structured result was recorded.".to_string()
}
}
ThreadEpiphanyReorientResultStatus::Failed => item_error
CoreEpiphanyCrrcResultStatus::Failed => item_error
.map(|error| format!("Reorientation worker failed: {error}"))
.unwrap_or_else(|| "Reorientation worker failed.".to_string()),
ThreadEpiphanyReorientResultStatus::Cancelled => {
CoreEpiphanyCrrcResultStatus::Cancelled => {
"Reorientation worker was cancelled before producing a result.".to_string()
}
ThreadEpiphanyReorientResultStatus::Running => {
CoreEpiphanyCrrcResultStatus::Running => {
"Reorientation worker is still running.".to_string()
}
ThreadEpiphanyReorientResultStatus::Pending => {
CoreEpiphanyCrrcResultStatus::Pending => {
"Reorientation worker has not produced a result yet.".to_string()
}
ThreadEpiphanyReorientResultStatus::MissingState => {
CoreEpiphanyCrrcResultStatus::MissingState => {
"No authoritative Epiphany state exists for this thread.".to_string()
}
ThreadEpiphanyReorientResultStatus::MissingBinding => {
CoreEpiphanyCrrcResultStatus::MissingBinding => {
"No matching Epiphany reorientation worker binding exists.".to_string()
}
ThreadEpiphanyReorientResultStatus::BackendUnavailable => {
CoreEpiphanyCrrcResultStatus::BackendUnavailable => {
"The bound runtime backend is unavailable.".to_string()
}
ThreadEpiphanyReorientResultStatus::BackendMissing => {
CoreEpiphanyCrrcResultStatus::BackendMissing => {
"The bound runtime backend job or item is missing.".to_string()
}
}

View file

@ -1,89 +1,63 @@
use codex_app_server_protocol::ThreadEpiphanyIndexResponse;
use codex_app_server_protocol::ThreadEpiphanyRetrieveIndexSummary;
use codex_app_server_protocol::ThreadEpiphanyRetrieveResponse;
use codex_app_server_protocol::ThreadEpiphanyRetrieveResult;
use codex_app_server_protocol::ThreadEpiphanyRetrieveResultKind;
use codex_app_server_protocol::ThreadEpiphanyRetrieveShardSummary;
use codex_core::CodexThread;
use codex_protocol::protocol::EpiphanyRetrievalState;
use codex_utils_absolute_path::AbsolutePathBuf;
use epiphany_core::EpiphanyRetrieveQuery;
use epiphany_core::EpiphanyRetrieveResponse as CoreEpiphanyRetrieveResponse;
use epiphany_core::EpiphanyRetrieveResult as CoreEpiphanyRetrieveResult;
use epiphany_core::EpiphanyRetrieveResultKind as CoreEpiphanyRetrieveResultKind;
use epiphany_core::index_workspace;
use epiphany_core::normalize_epiphany_retrieve_query;
use epiphany_core::retrieval_state_for_workspace;
use epiphany_core::retrieve_workspace;
use epiphany_state_model::EpiphanyRetrievalState;
use std::path::PathBuf;
pub fn map_epiphany_retrieve_response(
response: CoreEpiphanyRetrieveResponse,
) -> anyhow::Result<ThreadEpiphanyRetrieveResponse> {
Ok(ThreadEpiphanyRetrieveResponse {
query: response.query,
index_summary: map_epiphany_retrieve_index_summary(response.index_summary)?,
results: response
.results
.into_iter()
.map(map_epiphany_retrieve_result)
.collect(),
})
}
pub async fn index_thread_epiphany_retrieval(
thread: &CodexThread,
pub async fn index_epiphany_retrieval_for_paths(
workspace_root: PathBuf,
codex_home: PathBuf,
force_full_rebuild: bool,
) -> anyhow::Result<ThreadEpiphanyIndexResponse> {
let index_summary = thread
.epiphany_index(force_full_rebuild)
.await
.and_then(map_epiphany_retrieve_index_summary)?;
Ok(ThreadEpiphanyIndexResponse { index_summary })
) -> anyhow::Result<EpiphanyRetrievalState> {
index_epiphany_retrieval_state_for_paths(workspace_root, codex_home, force_full_rebuild).await
}
pub fn map_epiphany_retrieve_index_summary(
summary: EpiphanyRetrievalState,
) -> anyhow::Result<ThreadEpiphanyRetrieveIndexSummary> {
Ok(ThreadEpiphanyRetrieveIndexSummary {
workspace_root: AbsolutePathBuf::from_absolute_path(summary.workspace_root)
.map_err(anyhow::Error::from)?,
index_revision: summary.index_revision,
status: summary.status,
semantic_available: summary.semantic_available,
last_indexed_at_unix_seconds: summary.last_indexed_at_unix_seconds,
indexed_file_count: summary.indexed_file_count,
indexed_chunk_count: summary.indexed_chunk_count,
shards: summary
.shards
.into_iter()
.map(|shard| ThreadEpiphanyRetrieveShardSummary {
shard_id: shard.shard_id,
path_prefix: shard.path_prefix,
indexed_file_count: shard.indexed_file_count,
indexed_chunk_count: shard.indexed_chunk_count,
status: shard.status,
exact_available: shard.exact_available,
semantic_available: shard.semantic_available,
})
.collect(),
dirty_paths: summary.dirty_paths,
pub fn normalize_thread_epiphany_retrieve_query(
query: String,
limit: Option<u32>,
path_prefixes: Vec<PathBuf>,
) -> Result<EpiphanyRetrieveQuery, String> {
normalize_epiphany_retrieve_query(query, limit, path_prefixes).map_err(str::to_string)
}
pub async fn epiphany_retrieval_state_for_paths(
workspace_root: PathBuf,
codex_home: PathBuf,
) -> EpiphanyRetrievalState {
let fallback_workspace_root = workspace_root.clone();
let fallback_codex_home = codex_home.clone();
tokio::task::spawn_blocking(move || {
retrieval_state_for_workspace(&workspace_root, codex_home.as_path())
})
.await
.unwrap_or_else(|_| {
retrieval_state_for_workspace(&fallback_workspace_root, fallback_codex_home.as_path())
})
}
fn map_epiphany_retrieve_result(
result: CoreEpiphanyRetrieveResult,
) -> ThreadEpiphanyRetrieveResult {
ThreadEpiphanyRetrieveResult {
kind: match result.kind {
CoreEpiphanyRetrieveResultKind::ExactFile => {
ThreadEpiphanyRetrieveResultKind::ExactFile
}
CoreEpiphanyRetrieveResultKind::ExactDirectory => {
ThreadEpiphanyRetrieveResultKind::ExactDirectory
}
CoreEpiphanyRetrieveResultKind::SemanticChunk => {
ThreadEpiphanyRetrieveResultKind::SemanticChunk
}
},
path: result.path,
score: result.score,
line_start: result.line_start,
line_end: result.line_end,
excerpt: result.excerpt,
}
pub async fn index_epiphany_retrieval_state_for_paths(
workspace_root: PathBuf,
codex_home: PathBuf,
force_full_rebuild: bool,
) -> anyhow::Result<EpiphanyRetrievalState> {
tokio::task::spawn_blocking(move || {
index_workspace(&workspace_root, codex_home.as_path(), force_full_rebuild)
})
.await
.map_err(|err| anyhow::anyhow!("epiphany index worker failed: {err}"))?
}
pub async fn retrieve_epiphany_for_paths(
workspace_root: PathBuf,
codex_home: PathBuf,
query: EpiphanyRetrieveQuery,
) -> anyhow::Result<CoreEpiphanyRetrieveResponse> {
tokio::task::spawn_blocking(move || {
retrieve_workspace(&workspace_root, codex_home.as_path(), query)
})
.await
.map_err(|err| anyhow::anyhow!("epiphany retrieval worker failed: {err}"))?
}

View file

@ -0,0 +1,83 @@
use codex_app_server_protocol::ThreadEpiphanyIndexResponse;
use codex_app_server_protocol::ThreadEpiphanyRetrieveIndexSummary;
use codex_app_server_protocol::ThreadEpiphanyRetrieveResponse;
use codex_app_server_protocol::ThreadEpiphanyRetrieveResult;
use codex_app_server_protocol::ThreadEpiphanyRetrieveResultKind;
use codex_app_server_protocol::ThreadEpiphanyRetrieveShardSummary;
use codex_utils_absolute_path::AbsolutePathBuf;
use epiphany_core::EpiphanyRetrieveResponse as CoreEpiphanyRetrieveResponse;
use epiphany_core::EpiphanyRetrieveResult as CoreEpiphanyRetrieveResult;
use epiphany_core::EpiphanyRetrieveResultKind as CoreEpiphanyRetrieveResultKind;
use epiphany_state_model::EpiphanyRetrievalState;
pub fn protocol_retrieve_response(
response: CoreEpiphanyRetrieveResponse,
) -> anyhow::Result<ThreadEpiphanyRetrieveResponse> {
Ok(ThreadEpiphanyRetrieveResponse {
query: response.query,
index_summary: protocol_retrieve_index_summary(response.index_summary)?,
results: response
.results
.into_iter()
.map(protocol_retrieve_result)
.collect(),
})
}
pub fn protocol_index_response(
index_summary: EpiphanyRetrievalState,
) -> anyhow::Result<ThreadEpiphanyIndexResponse> {
Ok(ThreadEpiphanyIndexResponse {
index_summary: protocol_retrieve_index_summary(index_summary)?,
})
}
pub fn protocol_retrieve_index_summary(
summary: EpiphanyRetrievalState,
) -> anyhow::Result<ThreadEpiphanyRetrieveIndexSummary> {
Ok(ThreadEpiphanyRetrieveIndexSummary {
workspace_root: AbsolutePathBuf::from_absolute_path(summary.workspace_root)
.map_err(anyhow::Error::from)?,
index_revision: summary.index_revision,
status: summary.status,
semantic_available: summary.semantic_available,
last_indexed_at_unix_seconds: summary.last_indexed_at_unix_seconds,
indexed_file_count: summary.indexed_file_count,
indexed_chunk_count: summary.indexed_chunk_count,
shards: summary
.shards
.into_iter()
.map(|shard| ThreadEpiphanyRetrieveShardSummary {
shard_id: shard.shard_id,
path_prefix: shard.path_prefix,
indexed_file_count: shard.indexed_file_count,
indexed_chunk_count: shard.indexed_chunk_count,
status: shard.status,
exact_available: shard.exact_available,
semantic_available: shard.semantic_available,
})
.collect(),
dirty_paths: summary.dirty_paths,
})
}
fn protocol_retrieve_result(result: CoreEpiphanyRetrieveResult) -> ThreadEpiphanyRetrieveResult {
ThreadEpiphanyRetrieveResult {
kind: match result.kind {
CoreEpiphanyRetrieveResultKind::ExactFile => {
ThreadEpiphanyRetrieveResultKind::ExactFile
}
CoreEpiphanyRetrieveResultKind::ExactDirectory => {
ThreadEpiphanyRetrieveResultKind::ExactDirectory
}
CoreEpiphanyRetrieveResultKind::SemanticChunk => {
ThreadEpiphanyRetrieveResultKind::SemanticChunk
}
},
path: result.path,
score: result.score,
line_start: result.line_start,
line_end: result.line_end,
excerpt: result.excerpt,
}
}

View file

@ -1,15 +1,10 @@
use std::path::Path;
use codex_app_server_protocol::ThreadEpiphanyReorientFinding;
use codex_app_server_protocol::ThreadEpiphanyReorientResultStatus;
use codex_app_server_protocol::ThreadEpiphanyRoleFinding;
use codex_app_server_protocol::ThreadEpiphanyRoleId;
use codex_app_server_protocol::ThreadEpiphanyRoleResultStatus;
use codex_core::CodexThread;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::protocol::EpiphanyRuntimeLink;
use codex_protocol::protocol::EpiphanyThreadState;
use epiphany_core::EpiphanyCoordinatorRoleResultStatus as CoreEpiphanyCoordinatorRoleResultStatus;
use epiphany_core::EpiphanyCrrcResultStatus as CoreEpiphanyCrrcResultStatus;
use epiphany_core::EpiphanyReorientFindingInterpretation;
use epiphany_core::EpiphanyRoleFindingInterpretation;
use epiphany_core::EpiphanyRoleResultRoleId;
use epiphany_core::EpiphanyRuntimeJobSnapshot;
use epiphany_core::EpiphanyRuntimeJobStatus;
use epiphany_core::interpret_runtime_reorient_worker_result;
@ -17,234 +12,219 @@ use epiphany_core::interpret_runtime_role_worker_result;
use epiphany_core::runtime_job_snapshot;
use epiphany_core::runtime_reorient_worker_result;
use epiphany_core::runtime_role_worker_result;
use epiphany_state_model::EpiphanyRuntimeLink;
use epiphany_state_model::EpiphanyThreadState;
use crate::results::map_core_role_result_role_id;
use crate::results::map_protocol_reorient_finding;
use crate::results::map_protocol_role_finding;
use crate::results::render_epiphany_reorient_result_note;
use crate::results::render_epiphany_role_result_note;
use crate::error::EpiphanyBridgeError;
use crate::error::Result as BridgeResult;
use crate::results::render_core_reorient_result_note;
use crate::results::render_core_role_result_note;
pub fn role_finding_runtime_result_id(finding: &ThreadEpiphanyRoleFinding) -> Option<String> {
finding.runtime_result_id.clone()
#[derive(Debug, Clone)]
pub struct EpiphanyRoleResultSnapshot {
pub status: CoreEpiphanyCoordinatorRoleResultStatus,
pub finding: Option<EpiphanyRoleFindingInterpretation>,
pub note: String,
}
pub fn role_finding_runtime_job_id(finding: &ThreadEpiphanyRoleFinding) -> Option<String> {
finding.runtime_job_id.clone()
#[derive(Debug, Clone)]
pub struct EpiphanyReorientResultSnapshot {
pub status: CoreEpiphanyCrrcResultStatus,
pub finding: Option<EpiphanyReorientFindingInterpretation>,
pub note: String,
}
pub fn reorient_finding_runtime_result_id(
finding: &ThreadEpiphanyReorientFinding,
) -> Option<String> {
finding.runtime_result_id.clone()
}
pub fn reorient_finding_runtime_job_id(finding: &ThreadEpiphanyReorientFinding) -> Option<String> {
finding.runtime_job_id.clone()
}
pub fn load_epiphany_role_result_from_runtime_spine_job(
pub fn load_core_epiphany_role_result_from_runtime_spine_job(
job_id: &str,
runtime_store_path: Option<&Path>,
role_id: ThreadEpiphanyRoleId,
) -> (
ThreadEpiphanyRoleResultStatus,
Option<ThreadEpiphanyRoleFinding>,
String,
) {
role_id: EpiphanyRoleResultRoleId,
) -> EpiphanyRoleResultSnapshot {
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."
return EpiphanyRoleResultSnapshot {
status: CoreEpiphanyCoordinatorRoleResultStatus::Pending,
finding: None,
note: "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!(
return EpiphanyRoleResultSnapshot {
status: CoreEpiphanyCoordinatorRoleResultStatus::Pending,
finding: None,
note: format!(
"Heartbeat runtime job {:?} has not reported typed state yet.",
job_id
),
);
};
}
Err(err) => {
return (
ThreadEpiphanyRoleResultStatus::BackendUnavailable,
None,
format!(
return EpiphanyRoleResultSnapshot {
status: CoreEpiphanyCoordinatorRoleResultStatus::BackendUnavailable,
finding: None,
note: format!(
"Failed to read heartbeat runtime-spine job {:?}: {err}",
job_id
),
);
};
}
};
let status = map_runtime_role_result_status(&snapshot);
let finding = if status == ThreadEpiphanyRoleResultStatus::Completed {
let finding = if status == CoreEpiphanyCoordinatorRoleResultStatus::Completed {
match runtime_role_worker_result(runtime_store_path, job_id) {
Ok(Some(result)) => Some(map_protocol_role_finding(
role_id,
interpret_runtime_role_worker_result(
map_core_role_result_role_id(role_id),
&result,
),
)),
Ok(Some(result)) => Some(interpret_runtime_role_worker_result(role_id, &result)),
Ok(None) => {
return (
ThreadEpiphanyRoleResultStatus::BackendUnavailable,
None,
format!(
return EpiphanyRoleResultSnapshot {
status: CoreEpiphanyCoordinatorRoleResultStatus::BackendUnavailable,
finding: None,
note: format!(
"Heartbeat runtime job {:?} completed without an EpiphanyRuntimeRoleWorkerResult typed document; generic lifecycle receipts are not reviewable findings.",
job_id
),
);
};
}
Err(err) => {
return (
ThreadEpiphanyRoleResultStatus::BackendUnavailable,
None,
format!(
return EpiphanyRoleResultSnapshot {
status: CoreEpiphanyCoordinatorRoleResultStatus::BackendUnavailable,
finding: None,
note: format!(
"Failed to read typed role worker result for heartbeat runtime job {:?}: {err}",
job_id
),
);
};
}
}
} else {
None
};
let note = render_epiphany_role_result_note(role_id, status, finding.as_ref(), None);
(status, finding, note)
let note = render_core_role_result_note(role_id, status, finding.as_ref(), None);
EpiphanyRoleResultSnapshot {
status,
finding,
note,
}
}
pub fn load_epiphany_reorient_result_from_runtime_spine_job(
pub fn load_core_epiphany_reorient_result_from_runtime_spine_job(
job_id: &str,
runtime_store_path: Option<&Path>,
) -> (
ThreadEpiphanyReorientResultStatus,
Option<ThreadEpiphanyReorientFinding>,
String,
) {
) -> EpiphanyReorientResultSnapshot {
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."
return EpiphanyReorientResultSnapshot {
status: CoreEpiphanyCrrcResultStatus::Pending,
finding: None,
note: "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!(
return EpiphanyReorientResultSnapshot {
status: CoreEpiphanyCrrcResultStatus::Pending,
finding: None,
note: format!(
"Heartbeat runtime job {:?} has not reported typed state yet.",
job_id
),
);
};
}
Err(err) => {
return (
ThreadEpiphanyReorientResultStatus::BackendUnavailable,
None,
format!(
return EpiphanyReorientResultSnapshot {
status: CoreEpiphanyCrrcResultStatus::BackendUnavailable,
finding: None,
note: format!(
"Failed to read heartbeat runtime-spine job {:?}: {err}",
job_id
),
);
};
}
};
let status = map_runtime_reorient_result_status(&snapshot);
let finding = if status == ThreadEpiphanyReorientResultStatus::Completed {
let finding = if status == CoreEpiphanyCrrcResultStatus::Completed {
match runtime_reorient_worker_result(runtime_store_path, job_id) {
Ok(Some(result)) => Some(map_protocol_reorient_finding(
interpret_runtime_reorient_worker_result(&result),
)),
Ok(Some(result)) => Some(interpret_runtime_reorient_worker_result(&result)),
Ok(None) => {
return (
ThreadEpiphanyReorientResultStatus::BackendUnavailable,
None,
format!(
return EpiphanyReorientResultSnapshot {
status: CoreEpiphanyCrrcResultStatus::BackendUnavailable,
finding: None,
note: format!(
"Heartbeat runtime job {:?} completed without an EpiphanyRuntimeReorientWorkerResult typed document; generic lifecycle receipts are not reviewable findings.",
job_id
),
);
};
}
Err(err) => {
return (
ThreadEpiphanyReorientResultStatus::BackendUnavailable,
None,
format!(
return EpiphanyReorientResultSnapshot {
status: CoreEpiphanyCrrcResultStatus::BackendUnavailable,
finding: None,
note: format!(
"Failed to read typed reorientation worker result for heartbeat runtime job {:?}: {err}",
job_id
),
);
};
}
}
} else {
None
};
let note = render_epiphany_reorient_result_note(status, finding.as_ref(), None);
(status, finding, note)
let note = render_core_reorient_result_note(status, finding.as_ref(), None);
EpiphanyReorientResultSnapshot {
status,
finding,
note,
}
}
fn map_runtime_role_result_status(
snapshot: &EpiphanyRuntimeJobSnapshot,
) -> ThreadEpiphanyRoleResultStatus {
) -> CoreEpiphanyCoordinatorRoleResultStatus {
match snapshot.job.status {
EpiphanyRuntimeJobStatus::Queued => ThreadEpiphanyRoleResultStatus::Pending,
EpiphanyRuntimeJobStatus::Queued => CoreEpiphanyCoordinatorRoleResultStatus::Pending,
EpiphanyRuntimeJobStatus::Running | EpiphanyRuntimeJobStatus::WaitingForReview => {
ThreadEpiphanyRoleResultStatus::Running
CoreEpiphanyCoordinatorRoleResultStatus::Running
}
EpiphanyRuntimeJobStatus::Completed => {
if snapshot.result.is_some() {
ThreadEpiphanyRoleResultStatus::Completed
CoreEpiphanyCoordinatorRoleResultStatus::Completed
} else {
ThreadEpiphanyRoleResultStatus::Pending
CoreEpiphanyCoordinatorRoleResultStatus::Pending
}
}
EpiphanyRuntimeJobStatus::Failed => ThreadEpiphanyRoleResultStatus::Failed,
EpiphanyRuntimeJobStatus::Cancelled => ThreadEpiphanyRoleResultStatus::Cancelled,
EpiphanyRuntimeJobStatus::Failed => CoreEpiphanyCoordinatorRoleResultStatus::Failed,
EpiphanyRuntimeJobStatus::Cancelled => CoreEpiphanyCoordinatorRoleResultStatus::Cancelled,
}
}
fn map_runtime_reorient_result_status(
snapshot: &EpiphanyRuntimeJobSnapshot,
) -> ThreadEpiphanyReorientResultStatus {
) -> CoreEpiphanyCrrcResultStatus {
match snapshot.job.status {
EpiphanyRuntimeJobStatus::Queued => ThreadEpiphanyReorientResultStatus::Pending,
EpiphanyRuntimeJobStatus::Queued => CoreEpiphanyCrrcResultStatus::Pending,
EpiphanyRuntimeJobStatus::Running | EpiphanyRuntimeJobStatus::WaitingForReview => {
ThreadEpiphanyReorientResultStatus::Running
CoreEpiphanyCrrcResultStatus::Running
}
EpiphanyRuntimeJobStatus::Completed => {
if snapshot.result.is_some() {
ThreadEpiphanyReorientResultStatus::Completed
CoreEpiphanyCrrcResultStatus::Completed
} else {
ThreadEpiphanyReorientResultStatus::Pending
CoreEpiphanyCrrcResultStatus::Pending
}
}
EpiphanyRuntimeJobStatus::Failed => ThreadEpiphanyReorientResultStatus::Failed,
EpiphanyRuntimeJobStatus::Cancelled => ThreadEpiphanyReorientResultStatus::Cancelled,
EpiphanyRuntimeJobStatus::Failed => CoreEpiphanyCrrcResultStatus::Failed,
EpiphanyRuntimeJobStatus::Cancelled => CoreEpiphanyCrrcResultStatus::Cancelled,
}
}
pub async fn load_epiphany_role_result_snapshot(
pub async fn load_core_epiphany_role_result_snapshot(
state: &EpiphanyThreadState,
runtime_store_path: Option<&Path>,
role_id: ThreadEpiphanyRoleId,
role_id: EpiphanyRoleResultRoleId,
binding_id: &str,
) -> (
ThreadEpiphanyRoleResultStatus,
Option<ThreadEpiphanyRoleFinding>,
String,
) {
) -> EpiphanyRoleResultSnapshot {
if let Some(link) = latest_epiphany_runtime_link_for_binding(state, binding_id) {
return load_epiphany_role_result_from_runtime_spine_job(
return load_core_epiphany_role_result_from_runtime_spine_job(
link.runtime_job_id.as_str(),
runtime_store_path,
role_id,
@ -256,40 +236,39 @@ pub async fn load_epiphany_role_result_snapshot(
.iter()
.any(|binding| binding.id == binding_id)
{
return (
ThreadEpiphanyRoleResultStatus::MissingBinding,
None,
"No matching Epiphany role specialist binding exists.".to_string(),
);
return EpiphanyRoleResultSnapshot {
status: CoreEpiphanyCoordinatorRoleResultStatus::MissingBinding,
finding: None,
note: "No matching Epiphany role specialist binding exists.".to_string(),
};
}
EpiphanyRoleResultSnapshot {
status: CoreEpiphanyCoordinatorRoleResultStatus::BackendUnavailable,
finding: None,
note: "Role binding has no runtime-spine job id; launch a runtime-linked role worker for typed results.".to_string(),
}
(
ThreadEpiphanyRoleResultStatus::BackendUnavailable,
None,
"Role binding has no runtime-spine job id; launch a runtime-linked role worker for typed results.".to_string(),
)
}
pub async fn load_completed_epiphany_role_finding(
thread: &CodexThread,
pub fn load_completed_core_epiphany_role_finding(
runtime_store_path: Option<&Path>,
state: &EpiphanyThreadState,
role_id: ThreadEpiphanyRoleId,
role_id: EpiphanyRoleResultRoleId,
binding_id: &str,
) -> CodexResult<ThreadEpiphanyRoleFinding> {
) -> BridgeResult<EpiphanyRoleFindingInterpretation> {
if let Some(link) = latest_epiphany_runtime_link_for_binding(state, binding_id) {
let runtime_store_path = thread.epiphany_runtime_spine_store_path().await;
let (status, finding, _note) = load_epiphany_role_result_from_runtime_spine_job(
let snapshot = load_core_epiphany_role_result_from_runtime_spine_job(
link.runtime_job_id.as_str(),
Some(runtime_store_path.as_path()),
runtime_store_path,
role_id,
);
if status != ThreadEpiphanyRoleResultStatus::Completed {
return Err(CodexErr::InvalidRequest(format!(
if snapshot.status != CoreEpiphanyCoordinatorRoleResultStatus::Completed {
return Err(EpiphanyBridgeError::InvalidRequest(format!(
"cannot accept role result while worker status is {:?}",
status
snapshot.status
)));
}
return finding.ok_or_else(|| {
CodexErr::InvalidRequest(
return snapshot.finding.ok_or_else(|| {
EpiphanyBridgeError::InvalidRequest(
"cannot accept completed role worker because no typed runtime-spine result was recorded"
.to_string(),
)
@ -301,36 +280,32 @@ pub async fn load_completed_epiphany_role_finding(
.iter()
.any(|binding| binding.id == binding_id)
{
return Err(CodexErr::InvalidRequest(format!(
return Err(EpiphanyBridgeError::InvalidRequest(format!(
"epiphany role binding {:?} was not found",
binding_id
)));
}
Err(CodexErr::InvalidRequest(
Err(EpiphanyBridgeError::InvalidRequest(
"role findings without runtime-spine results are unsupported; accept only typed runtime-spine results"
.to_string(),
))
}
pub async fn load_epiphany_reorient_result_snapshot(
pub async fn load_core_epiphany_reorient_result_snapshot(
state: Option<&EpiphanyThreadState>,
runtime_store_path: Option<&Path>,
binding_id: &str,
) -> (
ThreadEpiphanyReorientResultStatus,
Option<ThreadEpiphanyReorientFinding>,
String,
) {
) -> EpiphanyReorientResultSnapshot {
let Some(state) = state else {
return (
ThreadEpiphanyReorientResultStatus::MissingState,
None,
"No authoritative Epiphany state exists for this thread.".to_string(),
);
return EpiphanyReorientResultSnapshot {
status: CoreEpiphanyCrrcResultStatus::MissingState,
finding: None,
note: "No authoritative Epiphany state exists for this thread.".to_string(),
};
};
if let Some(link) = latest_epiphany_runtime_link_for_binding(state, binding_id) {
return load_epiphany_reorient_result_from_runtime_spine_job(
return load_core_epiphany_reorient_result_from_runtime_spine_job(
link.runtime_job_id.as_str(),
runtime_store_path,
);
@ -341,38 +316,37 @@ pub async fn load_epiphany_reorient_result_snapshot(
.iter()
.any(|binding| binding.id == binding_id)
{
return (
ThreadEpiphanyReorientResultStatus::MissingBinding,
None,
"No matching Epiphany reorientation worker binding exists.".to_string(),
);
return EpiphanyReorientResultSnapshot {
status: CoreEpiphanyCrrcResultStatus::MissingBinding,
finding: None,
note: "No matching Epiphany reorientation worker binding exists.".to_string(),
};
}
EpiphanyReorientResultSnapshot {
status: CoreEpiphanyCrrcResultStatus::BackendUnavailable,
finding: None,
note: "Reorientation binding has no runtime-spine job id; launch a runtime-linked reorient worker for typed results.".to_string(),
}
(
ThreadEpiphanyReorientResultStatus::BackendUnavailable,
None,
"Reorientation binding has no runtime-spine job id; launch a runtime-linked reorient worker for typed results.".to_string(),
)
}
pub async fn load_completed_epiphany_reorient_finding(
thread: &CodexThread,
pub fn load_completed_core_epiphany_reorient_finding(
runtime_store_path: Option<&Path>,
state: &EpiphanyThreadState,
binding_id: &str,
) -> CodexResult<ThreadEpiphanyReorientFinding> {
) -> BridgeResult<EpiphanyReorientFindingInterpretation> {
if let Some(link) = latest_epiphany_runtime_link_for_binding(state, binding_id) {
let runtime_store_path = thread.epiphany_runtime_spine_store_path().await;
let (status, finding, _note) = load_epiphany_reorient_result_from_runtime_spine_job(
let snapshot = load_core_epiphany_reorient_result_from_runtime_spine_job(
link.runtime_job_id.as_str(),
Some(runtime_store_path.as_path()),
runtime_store_path,
);
if status != ThreadEpiphanyReorientResultStatus::Completed {
return Err(CodexErr::InvalidRequest(format!(
if snapshot.status != CoreEpiphanyCrrcResultStatus::Completed {
return Err(EpiphanyBridgeError::InvalidRequest(format!(
"cannot accept reorientation result while worker status is {:?}",
status
snapshot.status
)));
}
return finding.ok_or_else(|| {
CodexErr::InvalidRequest(
return snapshot.finding.ok_or_else(|| {
EpiphanyBridgeError::InvalidRequest(
"cannot accept completed reorientation worker because no typed runtime-spine result was recorded"
.to_string(),
)
@ -384,13 +358,13 @@ pub async fn load_completed_epiphany_reorient_finding(
.iter()
.any(|binding| binding.id == binding_id)
{
return Err(CodexErr::InvalidRequest(format!(
return Err(EpiphanyBridgeError::InvalidRequest(format!(
"epiphany reorientation binding {:?} was not found",
binding_id
)));
}
Err(CodexErr::InvalidRequest(
Err(EpiphanyBridgeError::InvalidRequest(
"reorientation findings without runtime-spine results are unsupported; accept only typed runtime-spine results"
.to_string(),
))
@ -412,9 +386,9 @@ mod tests {
use std::path::Path;
use std::path::PathBuf;
use codex_app_server_protocol::ThreadEpiphanyReorientResultStatus;
use codex_app_server_protocol::ThreadEpiphanyRoleId;
use codex_app_server_protocol::ThreadEpiphanyRoleResultStatus;
use epiphany_core::EpiphanyCoordinatorRoleResultStatus as CoreEpiphanyCoordinatorRoleResultStatus;
use epiphany_core::EpiphanyCrrcResultStatus as CoreEpiphanyCrrcResultStatus;
use epiphany_core::EpiphanyRoleResultRoleId;
use epiphany_core::RuntimeSpineInitOptions;
use epiphany_core::RuntimeSpineJobOptions;
use epiphany_core::RuntimeSpineJobResultOptions;
@ -433,15 +407,22 @@ mod tests {
let store = temp_store_path();
seed_completed_lifecycle_result(&store, "role-job", "modeling");
let (status, finding, note) = load_epiphany_role_result_from_runtime_spine_job(
let result = load_core_epiphany_role_result_from_runtime_spine_job(
"role-job",
Some(store.as_path()),
ThreadEpiphanyRoleId::Modeling,
EpiphanyRoleResultRoleId::Modeling,
);
assert_eq!(status, ThreadEpiphanyRoleResultStatus::BackendUnavailable);
assert!(finding.is_none());
assert!(note.contains("EpiphanyRuntimeRoleWorkerResult typed document"));
assert_eq!(
result.status,
CoreEpiphanyCoordinatorRoleResultStatus::BackendUnavailable
);
assert!(result.finding.is_none());
assert!(
result
.note
.contains("EpiphanyRuntimeRoleWorkerResult typed document")
);
let _ = fs::remove_file(store);
}
@ -451,17 +432,21 @@ mod tests {
let store = temp_store_path();
seed_completed_lifecycle_result(&store, "reorient-job", "reorientation");
let (status, finding, note) = load_epiphany_reorient_result_from_runtime_spine_job(
let result = load_core_epiphany_reorient_result_from_runtime_spine_job(
"reorient-job",
Some(store.as_path()),
);
assert_eq!(
status,
ThreadEpiphanyReorientResultStatus::BackendUnavailable
result.status,
CoreEpiphanyCrrcResultStatus::BackendUnavailable
);
assert!(result.finding.is_none());
assert!(
result
.note
.contains("EpiphanyRuntimeReorientWorkerResult typed document")
);
assert!(finding.is_none());
assert!(note.contains("EpiphanyRuntimeReorientWorkerResult typed document"));
let _ = fs::remove_file(store);
}

View file

@ -1,212 +1,16 @@
use codex_app_server_protocol::ThreadEpiphanyScene;
use codex_app_server_protocol::ThreadEpiphanySceneAction;
use codex_app_server_protocol::ThreadEpiphanySceneChurn;
use codex_app_server_protocol::ThreadEpiphanySceneGraph;
use codex_app_server_protocol::ThreadEpiphanySceneInvestigationCheckpoint;
use codex_app_server_protocol::ThreadEpiphanySceneRecord;
use codex_app_server_protocol::ThreadEpiphanySceneRecords;
use codex_app_server_protocol::ThreadEpiphanySceneRetrieval;
use codex_app_server_protocol::ThreadEpiphanySceneSource;
use codex_app_server_protocol::ThreadEpiphanySceneStateStatus;
use codex_app_server_protocol::ThreadEpiphanySceneStatusCount;
use codex_app_server_protocol::ThreadEpiphanySceneSubgoal;
use codex_protocol::protocol::EpiphanyThreadState;
use epiphany_core::EpiphanyScene as CoreEpiphanyScene;
use epiphany_core::EpiphanySceneAction as CoreEpiphanySceneAction;
use epiphany_core::EpiphanySceneChurn as CoreEpiphanySceneChurn;
use epiphany_core::EpiphanySceneGraph as CoreEpiphanySceneGraph;
use epiphany_core::EpiphanyScene;
use epiphany_core::EpiphanySceneInput;
use epiphany_core::EpiphanySceneInvestigationCheckpoint as CoreEpiphanySceneInvestigationCheckpoint;
use epiphany_core::EpiphanySceneRecord as CoreEpiphanySceneRecord;
use epiphany_core::EpiphanySceneRecords as CoreEpiphanySceneRecords;
use epiphany_core::EpiphanySceneRetrieval as CoreEpiphanySceneRetrieval;
use epiphany_core::EpiphanySceneSource as CoreEpiphanySceneSource;
use epiphany_core::EpiphanySceneStateStatus as CoreEpiphanySceneStateStatus;
use epiphany_core::EpiphanySceneStatusCount as CoreEpiphanySceneStatusCount;
use epiphany_core::EpiphanySceneSubgoal as CoreEpiphanySceneSubgoal;
use epiphany_core::derive_scene;
use epiphany_state_model::EpiphanyThreadState;
pub fn map_epiphany_scene(
pub fn derive_epiphany_scene(
state: Option<&EpiphanyThreadState>,
loaded: bool,
reorient_binding_id: &str,
) -> ThreadEpiphanyScene {
map_core_epiphany_scene(derive_scene(EpiphanySceneInput {
) -> EpiphanyScene {
derive_scene(EpiphanySceneInput {
state,
loaded,
reorient_binding_id,
}))
}
fn map_core_epiphany_scene(scene: CoreEpiphanyScene) -> ThreadEpiphanyScene {
ThreadEpiphanyScene {
state_status: match scene.state_status {
CoreEpiphanySceneStateStatus::Missing => ThreadEpiphanySceneStateStatus::Missing,
CoreEpiphanySceneStateStatus::Ready => ThreadEpiphanySceneStateStatus::Ready,
},
source: match scene.source {
CoreEpiphanySceneSource::Stored => ThreadEpiphanySceneSource::Stored,
CoreEpiphanySceneSource::Live => ThreadEpiphanySceneSource::Live,
},
revision: scene.revision,
objective: scene.objective,
active_subgoal: scene.active_subgoal.map(map_core_epiphany_scene_subgoal),
subgoals: scene
.subgoals
.into_iter()
.map(map_core_epiphany_scene_subgoal)
.collect(),
invariant_status_counts: scene
.invariant_status_counts
.into_iter()
.map(map_core_epiphany_scene_status_count)
.collect(),
graph: map_core_epiphany_scene_graph(scene.graph),
retrieval: scene.retrieval.map(map_core_epiphany_scene_retrieval),
investigation_checkpoint: scene
.investigation_checkpoint
.map(map_core_epiphany_scene_investigation_checkpoint),
observations: map_core_epiphany_scene_records(scene.observations),
evidence: map_core_epiphany_scene_records(scene.evidence),
churn: scene.churn.map(map_core_epiphany_scene_churn),
available_actions: scene
.available_actions
.into_iter()
.map(map_core_epiphany_scene_action)
.collect(),
}
}
pub fn map_core_epiphany_scene_action(
action: CoreEpiphanySceneAction,
) -> ThreadEpiphanySceneAction {
match action {
CoreEpiphanySceneAction::Index => ThreadEpiphanySceneAction::Index,
CoreEpiphanySceneAction::Retrieve => ThreadEpiphanySceneAction::Retrieve,
CoreEpiphanySceneAction::Distill => ThreadEpiphanySceneAction::Distill,
CoreEpiphanySceneAction::Context => ThreadEpiphanySceneAction::Context,
CoreEpiphanySceneAction::Planning => ThreadEpiphanySceneAction::Planning,
CoreEpiphanySceneAction::GraphQuery => ThreadEpiphanySceneAction::GraphQuery,
CoreEpiphanySceneAction::Jobs => ThreadEpiphanySceneAction::Jobs,
CoreEpiphanySceneAction::Roles => ThreadEpiphanySceneAction::Roles,
CoreEpiphanySceneAction::Coordinator => ThreadEpiphanySceneAction::Coordinator,
CoreEpiphanySceneAction::RoleLaunch => ThreadEpiphanySceneAction::RoleLaunch,
CoreEpiphanySceneAction::RoleResult => ThreadEpiphanySceneAction::RoleResult,
CoreEpiphanySceneAction::RoleAccept => ThreadEpiphanySceneAction::RoleAccept,
CoreEpiphanySceneAction::JobLaunch => ThreadEpiphanySceneAction::JobLaunch,
CoreEpiphanySceneAction::JobInterrupt => ThreadEpiphanySceneAction::JobInterrupt,
CoreEpiphanySceneAction::Freshness => ThreadEpiphanySceneAction::Freshness,
CoreEpiphanySceneAction::Pressure => ThreadEpiphanySceneAction::Pressure,
CoreEpiphanySceneAction::Reorient => ThreadEpiphanySceneAction::Reorient,
CoreEpiphanySceneAction::Crrc => ThreadEpiphanySceneAction::Crrc,
CoreEpiphanySceneAction::ReorientLaunch => ThreadEpiphanySceneAction::ReorientLaunch,
CoreEpiphanySceneAction::ReorientResult => ThreadEpiphanySceneAction::ReorientResult,
CoreEpiphanySceneAction::ReorientAccept => ThreadEpiphanySceneAction::ReorientAccept,
CoreEpiphanySceneAction::Propose => ThreadEpiphanySceneAction::Propose,
CoreEpiphanySceneAction::Promote => ThreadEpiphanySceneAction::Promote,
CoreEpiphanySceneAction::Update => ThreadEpiphanySceneAction::Update,
}
}
fn map_core_epiphany_scene_subgoal(
subgoal: CoreEpiphanySceneSubgoal,
) -> ThreadEpiphanySceneSubgoal {
ThreadEpiphanySceneSubgoal {
id: subgoal.id,
title: subgoal.title,
status: subgoal.status,
summary: subgoal.summary,
active: subgoal.active,
}
}
fn map_core_epiphany_scene_status_count(
count: CoreEpiphanySceneStatusCount,
) -> ThreadEpiphanySceneStatusCount {
ThreadEpiphanySceneStatusCount {
status: count.status,
count: count.count,
}
}
fn map_core_epiphany_scene_graph(graph: CoreEpiphanySceneGraph) -> ThreadEpiphanySceneGraph {
ThreadEpiphanySceneGraph {
architecture_node_count: graph.architecture_node_count,
architecture_edge_count: graph.architecture_edge_count,
dataflow_node_count: graph.dataflow_node_count,
dataflow_edge_count: graph.dataflow_edge_count,
link_count: graph.link_count,
active_node_ids: graph.active_node_ids,
active_edge_ids: graph.active_edge_ids,
open_question_count: graph.open_question_count,
open_gap_count: graph.open_gap_count,
dirty_paths: graph.dirty_paths,
checkpoint_id: graph.checkpoint_id,
checkpoint_summary: graph.checkpoint_summary,
}
}
fn map_core_epiphany_scene_retrieval(
retrieval: CoreEpiphanySceneRetrieval,
) -> ThreadEpiphanySceneRetrieval {
ThreadEpiphanySceneRetrieval {
workspace_root: retrieval.workspace_root,
status: retrieval.status,
semantic_available: retrieval.semantic_available,
index_revision: retrieval.index_revision,
indexed_file_count: retrieval.indexed_file_count,
indexed_chunk_count: retrieval.indexed_chunk_count,
shard_count: retrieval.shard_count,
dirty_path_count: retrieval.dirty_path_count,
}
}
fn map_core_epiphany_scene_investigation_checkpoint(
checkpoint: CoreEpiphanySceneInvestigationCheckpoint,
) -> ThreadEpiphanySceneInvestigationCheckpoint {
ThreadEpiphanySceneInvestigationCheckpoint {
checkpoint_id: checkpoint.checkpoint_id,
kind: checkpoint.kind,
disposition: checkpoint.disposition,
focus: checkpoint.focus,
summary: checkpoint.summary,
next_action: checkpoint.next_action,
captured_at_turn_id: checkpoint.captured_at_turn_id,
open_question_count: checkpoint.open_question_count,
code_ref_count: checkpoint.code_ref_count,
evidence_count: checkpoint.evidence_count,
}
}
fn map_core_epiphany_scene_records(
records: CoreEpiphanySceneRecords,
) -> ThreadEpiphanySceneRecords {
ThreadEpiphanySceneRecords {
total_count: records.total_count,
latest: records
.latest
.into_iter()
.map(map_core_epiphany_scene_record)
.collect(),
}
}
fn map_core_epiphany_scene_record(record: CoreEpiphanySceneRecord) -> ThreadEpiphanySceneRecord {
ThreadEpiphanySceneRecord {
id: record.id,
kind: record.kind,
status: record.status,
summary: record.summary,
code_ref_count: record.code_ref_count,
}
}
fn map_core_epiphany_scene_churn(churn: CoreEpiphanySceneChurn) -> ThreadEpiphanySceneChurn {
ThreadEpiphanySceneChurn {
understanding_status: churn.understanding_status,
diff_pressure: churn.diff_pressure,
graph_freshness: churn.graph_freshness,
warning: churn.warning,
unexplained_writes: churn.unexplained_writes,
}
})
}

View file

@ -0,0 +1,189 @@
use codex_app_server_protocol::ThreadEpiphanyScene;
use codex_app_server_protocol::ThreadEpiphanySceneAction;
use codex_app_server_protocol::ThreadEpiphanySceneChurn;
use codex_app_server_protocol::ThreadEpiphanySceneGraph;
use codex_app_server_protocol::ThreadEpiphanySceneInvestigationCheckpoint;
use codex_app_server_protocol::ThreadEpiphanySceneRecord;
use codex_app_server_protocol::ThreadEpiphanySceneRecords;
use codex_app_server_protocol::ThreadEpiphanySceneRetrieval;
use codex_app_server_protocol::ThreadEpiphanySceneSource;
use codex_app_server_protocol::ThreadEpiphanySceneStateStatus;
use codex_app_server_protocol::ThreadEpiphanySceneStatusCount;
use codex_app_server_protocol::ThreadEpiphanySceneSubgoal;
use epiphany_core::EpiphanyScene as CoreEpiphanyScene;
use epiphany_core::EpiphanySceneAction as CoreEpiphanySceneAction;
use epiphany_core::EpiphanySceneChurn as CoreEpiphanySceneChurn;
use epiphany_core::EpiphanySceneGraph as CoreEpiphanySceneGraph;
use epiphany_core::EpiphanySceneInvestigationCheckpoint as CoreEpiphanySceneInvestigationCheckpoint;
use epiphany_core::EpiphanySceneRecord as CoreEpiphanySceneRecord;
use epiphany_core::EpiphanySceneRecords as CoreEpiphanySceneRecords;
use epiphany_core::EpiphanySceneRetrieval as CoreEpiphanySceneRetrieval;
use epiphany_core::EpiphanySceneSource as CoreEpiphanySceneSource;
use epiphany_core::EpiphanySceneStateStatus as CoreEpiphanySceneStateStatus;
use epiphany_core::EpiphanySceneStatusCount as CoreEpiphanySceneStatusCount;
use epiphany_core::EpiphanySceneSubgoal as CoreEpiphanySceneSubgoal;
pub fn protocol_scene(scene: CoreEpiphanyScene) -> ThreadEpiphanyScene {
ThreadEpiphanyScene {
state_status: match scene.state_status {
CoreEpiphanySceneStateStatus::Missing => ThreadEpiphanySceneStateStatus::Missing,
CoreEpiphanySceneStateStatus::Ready => ThreadEpiphanySceneStateStatus::Ready,
},
source: match scene.source {
CoreEpiphanySceneSource::Stored => ThreadEpiphanySceneSource::Stored,
CoreEpiphanySceneSource::Live => ThreadEpiphanySceneSource::Live,
},
revision: scene.revision,
objective: scene.objective,
active_subgoal: scene.active_subgoal.map(protocol_scene_subgoal),
subgoals: scene
.subgoals
.into_iter()
.map(protocol_scene_subgoal)
.collect(),
invariant_status_counts: scene
.invariant_status_counts
.into_iter()
.map(protocol_scene_status_count)
.collect(),
graph: protocol_scene_graph(scene.graph),
retrieval: scene.retrieval.map(protocol_scene_retrieval),
investigation_checkpoint: scene
.investigation_checkpoint
.map(protocol_scene_investigation_checkpoint),
observations: protocol_scene_records(scene.observations),
evidence: protocol_scene_records(scene.evidence),
churn: scene.churn.map(protocol_scene_churn),
available_actions: scene
.available_actions
.into_iter()
.map(protocol_scene_action)
.collect(),
}
}
pub fn protocol_scene_action(action: CoreEpiphanySceneAction) -> ThreadEpiphanySceneAction {
match action {
CoreEpiphanySceneAction::Index => ThreadEpiphanySceneAction::Index,
CoreEpiphanySceneAction::Retrieve => ThreadEpiphanySceneAction::Retrieve,
CoreEpiphanySceneAction::Distill => ThreadEpiphanySceneAction::Distill,
CoreEpiphanySceneAction::Context => ThreadEpiphanySceneAction::Context,
CoreEpiphanySceneAction::Planning => ThreadEpiphanySceneAction::Planning,
CoreEpiphanySceneAction::GraphQuery => ThreadEpiphanySceneAction::GraphQuery,
CoreEpiphanySceneAction::Jobs => ThreadEpiphanySceneAction::Jobs,
CoreEpiphanySceneAction::Roles => ThreadEpiphanySceneAction::Roles,
CoreEpiphanySceneAction::Coordinator => ThreadEpiphanySceneAction::Coordinator,
CoreEpiphanySceneAction::RoleLaunch => ThreadEpiphanySceneAction::RoleLaunch,
CoreEpiphanySceneAction::RoleResult => ThreadEpiphanySceneAction::RoleResult,
CoreEpiphanySceneAction::RoleAccept => ThreadEpiphanySceneAction::RoleAccept,
CoreEpiphanySceneAction::JobLaunch => ThreadEpiphanySceneAction::JobLaunch,
CoreEpiphanySceneAction::JobInterrupt => ThreadEpiphanySceneAction::JobInterrupt,
CoreEpiphanySceneAction::Freshness => ThreadEpiphanySceneAction::Freshness,
CoreEpiphanySceneAction::Pressure => ThreadEpiphanySceneAction::Pressure,
CoreEpiphanySceneAction::Reorient => ThreadEpiphanySceneAction::Reorient,
CoreEpiphanySceneAction::Crrc => ThreadEpiphanySceneAction::Crrc,
CoreEpiphanySceneAction::ReorientLaunch => ThreadEpiphanySceneAction::ReorientLaunch,
CoreEpiphanySceneAction::ReorientResult => ThreadEpiphanySceneAction::ReorientResult,
CoreEpiphanySceneAction::ReorientAccept => ThreadEpiphanySceneAction::ReorientAccept,
CoreEpiphanySceneAction::Propose => ThreadEpiphanySceneAction::Propose,
CoreEpiphanySceneAction::Promote => ThreadEpiphanySceneAction::Promote,
CoreEpiphanySceneAction::Update => ThreadEpiphanySceneAction::Update,
}
}
fn protocol_scene_subgoal(subgoal: CoreEpiphanySceneSubgoal) -> ThreadEpiphanySceneSubgoal {
ThreadEpiphanySceneSubgoal {
id: subgoal.id,
title: subgoal.title,
status: subgoal.status,
summary: subgoal.summary,
active: subgoal.active,
}
}
fn protocol_scene_status_count(
count: CoreEpiphanySceneStatusCount,
) -> ThreadEpiphanySceneStatusCount {
ThreadEpiphanySceneStatusCount {
status: count.status,
count: count.count,
}
}
fn protocol_scene_graph(graph: CoreEpiphanySceneGraph) -> ThreadEpiphanySceneGraph {
ThreadEpiphanySceneGraph {
architecture_node_count: graph.architecture_node_count,
architecture_edge_count: graph.architecture_edge_count,
dataflow_node_count: graph.dataflow_node_count,
dataflow_edge_count: graph.dataflow_edge_count,
link_count: graph.link_count,
active_node_ids: graph.active_node_ids,
active_edge_ids: graph.active_edge_ids,
open_question_count: graph.open_question_count,
open_gap_count: graph.open_gap_count,
dirty_paths: graph.dirty_paths,
checkpoint_id: graph.checkpoint_id,
checkpoint_summary: graph.checkpoint_summary,
}
}
fn protocol_scene_retrieval(retrieval: CoreEpiphanySceneRetrieval) -> ThreadEpiphanySceneRetrieval {
ThreadEpiphanySceneRetrieval {
workspace_root: retrieval.workspace_root,
status: retrieval.status,
semantic_available: retrieval.semantic_available,
index_revision: retrieval.index_revision,
indexed_file_count: retrieval.indexed_file_count,
indexed_chunk_count: retrieval.indexed_chunk_count,
shard_count: retrieval.shard_count,
dirty_path_count: retrieval.dirty_path_count,
}
}
fn protocol_scene_investigation_checkpoint(
checkpoint: CoreEpiphanySceneInvestigationCheckpoint,
) -> ThreadEpiphanySceneInvestigationCheckpoint {
ThreadEpiphanySceneInvestigationCheckpoint {
checkpoint_id: checkpoint.checkpoint_id,
kind: checkpoint.kind,
disposition: checkpoint.disposition,
focus: checkpoint.focus,
summary: checkpoint.summary,
next_action: checkpoint.next_action,
captured_at_turn_id: checkpoint.captured_at_turn_id,
open_question_count: checkpoint.open_question_count,
code_ref_count: checkpoint.code_ref_count,
evidence_count: checkpoint.evidence_count,
}
}
fn protocol_scene_records(records: CoreEpiphanySceneRecords) -> ThreadEpiphanySceneRecords {
ThreadEpiphanySceneRecords {
total_count: records.total_count,
latest: records
.latest
.into_iter()
.map(protocol_scene_record)
.collect(),
}
}
fn protocol_scene_record(record: CoreEpiphanySceneRecord) -> ThreadEpiphanySceneRecord {
ThreadEpiphanySceneRecord {
id: record.id,
kind: record.kind,
status: record.status,
summary: record.summary,
code_ref_count: record.code_ref_count,
}
}
fn protocol_scene_churn(churn: CoreEpiphanySceneChurn) -> ThreadEpiphanySceneChurn {
ThreadEpiphanySceneChurn {
understanding_status: churn.understanding_status,
diff_pressure: churn.diff_pressure,
graph_freshness: churn.graph_freshness,
warning: churn.warning,
unexplained_writes: churn.unexplained_writes,
}
}

View file

@ -2,57 +2,29 @@ use std::fs;
use std::path::Path;
use std::path::PathBuf;
use codex_core::CodexThread;
use codex_core::RolloutRecorder;
use codex_core::latest_epiphany_state_from_rollout_items;
use codex_protocol::protocol::EpiphanyThreadState;
use codex_protocol::protocol::InitialHistory;
use epiphany_core::write_thread_state;
use epiphany_state_model::EpiphanyThreadState;
use tracing::warn;
pub async fn load_epiphany_state_from_rollout_path(
rollout_path: &Path,
) -> std::result::Result<Option<EpiphanyThreadState>, String> {
let items = match RolloutRecorder::get_rollout_history(rollout_path)
.await
.map_err(|err| {
format!(
"failed to load rollout `{}` for Epiphany state: {err}",
rollout_path.display()
)
})? {
InitialHistory::New | InitialHistory::Cleared => Vec::new(),
InitialHistory::Forked(items) => items,
InitialHistory::Resumed(resumed) => resumed.history,
};
Ok(latest_epiphany_state_from_rollout_items(&items))
}
use crate::retrieve::epiphany_retrieval_state_for_paths;
pub async fn live_thread_epiphany_state(thread: &CodexThread) -> Option<EpiphanyThreadState> {
let mut epiphany_state = thread.epiphany_state().await;
if let Some(state) = epiphany_state.as_mut()
&& state.retrieval.is_none()
{
state.retrieval = Some(thread.epiphany_retrieval_state().await);
}
if let Some(state) = epiphany_state.as_ref() {
let config = thread.config_snapshot().await;
let thread_id = thread_state_mirror_id(thread);
if let Err(err) = mirror_thread_state_to_workspace(config.cwd.as_path(), &thread_id, state)
{
warn!(
"failed to mirror Epiphany thread state into native store: {err}"
);
}
}
epiphany_state
}
pub async fn client_visible_live_thread_epiphany_state(
thread: &CodexThread,
fallback: EpiphanyThreadState,
pub async fn client_visible_epiphany_state_for_paths(
mut state: EpiphanyThreadState,
workspace_root: &Path,
codex_home: PathBuf,
mirror_thread_id: Option<&str>,
) -> EpiphanyThreadState {
live_thread_epiphany_state(thread).await.unwrap_or(fallback)
if state.retrieval.is_none() {
state.retrieval = Some(
epiphany_retrieval_state_for_paths(workspace_root.to_path_buf(), codex_home).await,
);
}
if let Some(thread_id) = mirror_thread_id
&& let Err(err) = mirror_thread_state_to_workspace(workspace_root, thread_id, &state)
{
warn!("failed to mirror Epiphany thread state into native store: {err}");
}
state
}
pub fn thread_state_store_path(workspace_root: &Path) -> PathBuf {
@ -72,13 +44,10 @@ pub fn mirror_thread_state_to_workspace(
Ok(store_path)
}
fn thread_state_mirror_id(thread: &CodexThread) -> String {
thread
.rollout_path()
.and_then(|path| {
path.file_stem()
.and_then(|stem| stem.to_str())
.map(str::to_string)
})
pub fn thread_state_mirror_id_from_rollout_path(rollout_path: Option<&Path>) -> String {
rollout_path
.and_then(|path| path.file_stem())
.and_then(|stem| stem.to_str())
.map(str::to_string)
.unwrap_or_else(|| "live-thread".to_string())
}

View file

@ -1,77 +0,0 @@
use codex_app_server_protocol::Thread;
use codex_app_server_protocol::ThreadHistoryBuilder;
use codex_app_server_protocol::TurnStatus;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::TokenUsageInfo;
pub fn latest_token_usage_info_from_rollout_items(
rollout_items: &[RolloutItem],
) -> Option<TokenUsageInfo> {
rollout_items.iter().rev().find_map(|item| match item {
RolloutItem::EventMsg(EventMsg::TokenCount(event)) => event.info.clone(),
_ => None,
})
}
/// Identifies the turn that was active when a `TokenCount` record appeared.
///
/// The id is preferred when it still appears in the rebuilt thread. The position
/// is a fallback for histories whose implicit turn ids are regenerated during
/// reconstruction.
struct TokenUsageTurnOwner {
id: String,
position: Option<usize>,
}
pub fn latest_token_usage_turn_id_from_rollout_items(
rollout_items: &[RolloutItem],
thread: &Thread,
) -> Option<String> {
let owner = latest_token_usage_turn_owner_from_rollout_items(rollout_items)?;
if thread.turns.iter().any(|turn| turn.id == owner.id) {
return Some(owner.id);
}
owner
.position
.and_then(|position| thread.turns.get(position))
.map(|turn| turn.id.clone())
}
fn latest_token_usage_turn_owner_from_rollout_items(
rollout_items: &[RolloutItem],
) -> Option<TokenUsageTurnOwner> {
let mut builder = ThreadHistoryBuilder::new();
let mut token_usage_turn_owner = None;
for item in rollout_items {
if matches!(item, RolloutItem::EventMsg(EventMsg::TokenCount(_))) {
token_usage_turn_owner =
builder
.active_turn_snapshot()
.map(|turn| TokenUsageTurnOwner {
id: turn.id,
position: builder.active_turn_position(),
});
}
builder.handle_rollout_item(item);
}
token_usage_turn_owner
}
/// Chooses a fallback turn id that should own a replayed token usage update.
///
/// Normal replay derives the owner from the rollout position of the latest
/// `TokenCount` event. This fallback only preserves a stable wire shape for
/// unusual histories where that rollout information cannot be read.
pub fn latest_token_usage_turn_id(thread: &Thread) -> String {
thread
.turns
.iter()
.rev()
.find(|turn| matches!(turn.status, TurnStatus::Completed | TurnStatus::Failed))
.or_else(|| thread.turns.last())
.map(|turn| turn.id.clone())
.unwrap_or_default()
}

View file

@ -1,73 +1,62 @@
use std::path::Path;
use codex_app_server_protocol::*;
use codex_protocol::protocol::EpiphanyRetrievalState;
use codex_protocol::protocol::EpiphanyThreadState;
use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo;
use epiphany_core::EpiphanyContextParams;
use epiphany_core::EpiphanyDistillInput;
use epiphany_core::EpiphanyGraphQuery;
use epiphany_core::EpiphanyMapProposalInput;
use epiphany_core::EpiphanyRoleResultRoleId;
use epiphany_core::EpiphanySceneInput;
use epiphany_core::EpiphanyTokenUsageSnapshot;
use epiphany_core::EpiphanyViewLens;
use epiphany_core::derive_scene;
use epiphany_core::distill_observation;
use epiphany_core::epiphany_view_needs_jobs;
use epiphany_core::epiphany_view_needs_pressure;
use epiphany_core::epiphany_view_needs_reorientation_inputs;
use epiphany_core::propose_map_update;
use epiphany_core::reorient_finding_already_accepted;
use epiphany_state_model::EpiphanyRetrievalState;
use epiphany_state_model::EpiphanyThreadState;
use crate::context::map_epiphany_context;
use crate::context::map_epiphany_graph_query;
use crate::context::map_epiphany_planning;
use crate::context::derive_epiphany_context;
use crate::context::derive_epiphany_graph_query;
use crate::context::derive_epiphany_planning;
use crate::context_protocol::protocol_context_view;
use crate::context_protocol::protocol_graph_query_view;
use crate::context_protocol::protocol_planning_view;
use crate::coordinator::derive_epiphany_coordinator_status;
use crate::coordinator::epiphany_reorient_finding_already_accepted;
use crate::coordinator::map_epiphany_coordinator_view;
use crate::coordinator::map_epiphany_crrc_recommendation;
use crate::coordinator::map_epiphany_roles;
use crate::coordinator::render_epiphany_roles_note;
use crate::coordinator_protocol::protocol_coordinator_view;
use crate::coordinator_protocol::protocol_crrc_recommendation;
use crate::coordinator_protocol::protocol_role_board_lanes;
use crate::cultnet::EpiphanyFreshnessSurface;
use crate::cultnet::EpiphanySurfaceSource;
use crate::jobs::map_epiphany_jobs;
use crate::launch::EPIPHANY_REORIENT_LAUNCH_BINDING_ID;
use crate::pressure::map_epiphany_pressure;
use crate::pressure::derive_epiphany_pressure;
use crate::protocol_edge::protocol_job_from_surface;
use crate::protocol_edge::protocol_pressure_from_core;
use crate::protocol_edge::protocol_reorient_decision;
use crate::protocol_edge::protocol_reorient_finding;
use crate::protocol_edge::protocol_reorient_result_status;
use crate::protocol_edge::protocol_reorient_source;
use crate::protocol_edge::protocol_reorient_state_status;
use crate::protocol_edge::protocol_role_finding;
use crate::protocol_edge::protocol_role_id_from_core;
use crate::protocol_edge::protocol_role_result_status;
use crate::protocol_edge::protocol_roles_source;
use crate::protocol_edge::protocol_view_lens_from_core;
use crate::reorient::EpiphanyFreshnessWatcherSnapshot;
use crate::reorient::map_epiphany_freshness;
use crate::reorient::map_epiphany_reorient;
use crate::runtime_results::load_epiphany_reorient_result_snapshot;
use crate::runtime_results::load_epiphany_role_result_snapshot;
use crate::scene::map_core_epiphany_scene_action;
use crate::scene::map_epiphany_scene;
pub fn default_epiphany_view_lenses() -> Vec<ThreadEpiphanyViewLens> {
vec![
ThreadEpiphanyViewLens::Scene,
ThreadEpiphanyViewLens::Jobs,
ThreadEpiphanyViewLens::Roles,
ThreadEpiphanyViewLens::Planning,
ThreadEpiphanyViewLens::Pressure,
ThreadEpiphanyViewLens::Reorient,
ThreadEpiphanyViewLens::Crrc,
ThreadEpiphanyViewLens::Coordinator,
]
}
pub fn epiphany_view_needs_jobs(lenses: &[ThreadEpiphanyViewLens]) -> bool {
lenses.contains(&ThreadEpiphanyViewLens::Jobs)
|| lenses.contains(&ThreadEpiphanyViewLens::Roles)
|| lenses.contains(&ThreadEpiphanyViewLens::Crrc)
|| lenses.contains(&ThreadEpiphanyViewLens::Coordinator)
}
pub fn epiphany_view_needs_reorientation_inputs(lenses: &[ThreadEpiphanyViewLens]) -> bool {
lenses.contains(&ThreadEpiphanyViewLens::Roles)
|| lenses.contains(&ThreadEpiphanyViewLens::Reorient)
|| lenses.contains(&ThreadEpiphanyViewLens::Crrc)
|| lenses.contains(&ThreadEpiphanyViewLens::Coordinator)
}
pub fn epiphany_view_needs_pressure(lenses: &[ThreadEpiphanyViewLens]) -> bool {
lenses.contains(&ThreadEpiphanyViewLens::Pressure)
|| epiphany_view_needs_reorientation_inputs(lenses)
}
pub fn epiphany_view_needs_runtime_store(lenses: &[ThreadEpiphanyViewLens]) -> bool {
lenses.contains(&ThreadEpiphanyViewLens::Roles)
|| lenses.contains(&ThreadEpiphanyViewLens::Crrc)
|| lenses.contains(&ThreadEpiphanyViewLens::Coordinator)
}
use crate::reorient::derive_epiphany_freshness_view;
use crate::reorient::derive_epiphany_reorient;
use crate::runtime_results::load_core_epiphany_reorient_result_snapshot;
use crate::runtime_results::load_core_epiphany_role_result_snapshot;
use crate::scene::derive_epiphany_scene;
use crate::scene_protocol::protocol_scene;
use crate::scene_protocol::protocol_scene_action;
pub struct EpiphanyFreshnessResponseInput<'a> {
pub thread_id: String,
@ -77,25 +66,25 @@ pub struct EpiphanyFreshnessResponseInput<'a> {
pub watcher_snapshot: Option<EpiphanyFreshnessWatcherSnapshot<'a>>,
}
pub fn map_epiphany_freshness_response(
pub fn derive_epiphany_freshness_surface(
input: EpiphanyFreshnessResponseInput<'_>,
) -> ThreadEpiphanyFreshnessResponse {
let (state_revision, retrieval, graph, watcher) = map_epiphany_freshness(
) -> EpiphanyFreshnessSurface {
let freshness = derive_epiphany_freshness_view(
input.state,
input.retrieval_override,
input.watcher_snapshot,
);
ThreadEpiphanyFreshnessResponse {
EpiphanyFreshnessSurface {
thread_id: input.thread_id,
source: if input.loaded {
ThreadEpiphanyFreshnessSource::Live
EpiphanySurfaceSource::Live
} else {
ThreadEpiphanyFreshnessSource::Stored
EpiphanySurfaceSource::Stored
},
state_revision,
retrieval,
graph,
watcher,
state_revision: freshness.state_revision,
retrieval: freshness.retrieval,
graph: freshness.graph,
watcher: freshness.watcher,
}
}
@ -103,9 +92,10 @@ pub fn map_epiphany_context_response(
thread_id: String,
loaded: bool,
state: Option<&EpiphanyThreadState>,
params: &ThreadEpiphanyContextParams,
params: &EpiphanyContextParams,
) -> ThreadEpiphanyContextResponse {
let (state_status, state_revision, context, missing) = map_epiphany_context(state, params);
let (state_status, state_revision, context, missing) =
protocol_context_view(derive_epiphany_context(state, params));
ThreadEpiphanyContextResponse {
thread_id,
source: if loaded {
@ -124,10 +114,10 @@ pub fn map_epiphany_graph_query_response(
thread_id: String,
loaded: bool,
state: Option<&EpiphanyThreadState>,
query: &ThreadEpiphanyGraphQuery,
query: &EpiphanyGraphQuery,
) -> ThreadEpiphanyGraphQueryResponse {
let (state_status, state_revision, graph, frontier, checkpoint, matched, missing) =
map_epiphany_graph_query(state, query);
protocol_graph_query_view(derive_epiphany_graph_query(state, query));
ThreadEpiphanyGraphQueryResponse {
thread_id,
source: if loaded {
@ -147,12 +137,12 @@ pub fn map_epiphany_graph_query_response(
pub struct EpiphanyViewResponseInput<'a> {
pub thread_id: String,
pub lenses: Vec<ThreadEpiphanyViewLens>,
pub lenses: Vec<EpiphanyViewLens>,
pub loaded: bool,
pub state: Option<&'a EpiphanyThreadState>,
pub retrieval_override: Option<&'a EpiphanyRetrievalState>,
pub watcher_snapshot: Option<EpiphanyFreshnessWatcherSnapshot<'a>>,
pub token_usage_info: Option<&'a CoreTokenUsageInfo>,
pub token_usage_info: Option<&'a EpiphanyTokenUsageSnapshot>,
pub runtime_store_path: Option<&'a Path>,
}
@ -173,19 +163,34 @@ pub async fn map_epiphany_view_response(
let needs_jobs = epiphany_view_needs_jobs(&lenses);
let needs_reorientation_inputs = epiphany_view_needs_reorientation_inputs(&lenses);
let needs_pressure = epiphany_view_needs_pressure(&lenses);
let pressure = needs_pressure.then(|| map_epiphany_pressure(token_usage_info));
let freshness = needs_reorientation_inputs
.then(|| map_epiphany_freshness(state, retrieval_override, watcher_snapshot));
let (state_revision, reorient_state_status, reorient_decision) =
if let (Some((state_revision, retrieval, graph, watcher)), Some(pressure)) =
(freshness.as_ref(), pressure.as_ref())
let core_pressure = needs_pressure.then(|| derive_epiphany_pressure(token_usage_info));
let pressure = needs_pressure
.then(|| protocol_pressure_from_core(derive_epiphany_pressure(token_usage_info)));
let core_freshness = needs_reorientation_inputs
.then(|| derive_epiphany_freshness_view(state, retrieval_override, watcher_snapshot));
let (state_revision, core_reorient_state_status, core_reorient_decision) =
if let (Some(freshness), Some(core_pressure)) =
(core_freshness.as_ref(), core_pressure.as_ref())
{
let (state_status, decision) =
map_epiphany_reorient(state, pressure, retrieval, graph, watcher);
(*state_revision, state_status, Some(decision))
let (state_status, decision) = derive_epiphany_reorient(
state,
core_pressure,
&freshness.retrieval,
&freshness.graph,
&freshness.watcher,
);
(freshness.state_revision, state_status, Some(decision))
} else {
(None, ThreadEpiphanyReorientStateStatus::Missing, None)
(
None,
epiphany_core::EpiphanyReorientStateStatus::Missing,
None,
)
};
let reorient_state_status = protocol_reorient_state_status(core_reorient_state_status);
let reorient_decision = core_reorient_decision
.clone()
.map(protocol_reorient_decision);
let jobs = if needs_jobs {
map_epiphany_jobs(state, retrieval_override)
} else {
@ -195,85 +200,94 @@ pub async fn map_epiphany_view_response(
.iter()
.find(|job| job.id == EPIPHANY_REORIENT_LAUNCH_BINDING_ID)
.cloned();
let (reorient_result_status, reorient_finding, reorient_result_note) = if runtime_store_path
.is_some()
|| lenses.contains(&ThreadEpiphanyViewLens::Crrc)
|| lenses.contains(&ThreadEpiphanyViewLens::Coordinator)
let reorient_result = if runtime_store_path.is_some()
|| lenses.contains(&EpiphanyViewLens::Crrc)
|| lenses.contains(&EpiphanyViewLens::Coordinator)
{
load_epiphany_reorient_result_snapshot(
load_core_epiphany_reorient_result_snapshot(
state,
runtime_store_path,
EPIPHANY_REORIENT_LAUNCH_BINDING_ID,
)
.await
} else {
(
ThreadEpiphanyReorientResultStatus::MissingState,
None,
"Reorient result was not requested.".to_string(),
)
crate::runtime_results::EpiphanyReorientResultSnapshot {
status: epiphany_core::EpiphanyCrrcResultStatus::MissingState,
finding: None,
note: "Reorient result was not requested.".to_string(),
}
};
let reorient_result_status = protocol_reorient_result_status(reorient_result.status);
let reorient_finding = reorient_result
.finding
.clone()
.map(protocol_reorient_finding);
let reorient_result_note = reorient_result.note.clone();
let checkpoint_present = state
.and_then(|state| state.investigation_checkpoint.as_ref())
.is_some();
let reorient_finding_accepted = reorient_finding.as_ref().is_some_and(|finding| {
state.is_some_and(|state| epiphany_reorient_finding_already_accepted(state, finding))
let reorient_finding_accepted = reorient_result.finding.as_ref().is_some_and(|finding| {
state.is_some_and(|state| reorient_finding_already_accepted(state, finding))
});
let recommendation =
if let (Some(pressure), Some(decision)) = (pressure.as_ref(), reorient_decision.as_ref()) {
Some(map_epiphany_crrc_recommendation(
loaded,
reorient_state_status,
pressure,
decision,
reorient_result_status,
checkpoint_present,
reorient_finding.is_some(),
reorient_finding_accepted,
))
} else {
None
};
let roles = if let (Some(pressure), Some(decision), Some(recommendation)) = (
pressure.as_ref(),
reorient_decision.as_ref(),
let recommendation = if let (Some(core_pressure), Some(decision)) =
(core_pressure.as_ref(), core_reorient_decision.as_ref())
{
Some(map_epiphany_crrc_recommendation(
loaded,
core_reorient_state_status,
core_pressure,
decision,
reorient_result.status,
checkpoint_present,
reorient_finding.is_some(),
reorient_finding_accepted,
))
} else {
None
};
let roles = if let (Some(core_pressure), Some(decision), Some(recommendation)) = (
core_pressure.as_ref(),
core_reorient_decision.as_ref(),
recommendation.as_ref(),
) {
Some(map_epiphany_roles(
state,
&jobs,
decision,
pressure,
core_pressure,
recommendation,
reorient_result_status,
reorient_result.status,
reorient_job.as_ref(),
))
} else {
None
};
let coordinator_response = if lenses.contains(&ThreadEpiphanyViewLens::Coordinator) {
if let (Some(pressure), Some(recommendation), Some(roles)) =
(pressure.as_ref(), recommendation.as_ref(), roles.clone())
{
let coordinator_response = if lenses.contains(&EpiphanyViewLens::Coordinator) {
if let (Some(core_pressure), Some(recommendation), Some(roles)) = (
core_pressure.as_ref(),
recommendation.as_ref(),
roles.clone(),
) {
let status = derive_epiphany_coordinator_status(
state,
runtime_store_path,
reorient_state_status,
pressure,
core_reorient_state_status,
core_pressure,
recommendation,
roles,
reorient_decision.as_ref(),
reorient_result_status,
reorient_finding.as_ref(),
roles.roles.clone(),
core_reorient_decision.as_ref(),
reorient_result.status,
reorient_result.finding.as_ref(),
checkpoint_present,
)
.await;
Some(map_epiphany_coordinator_view(
Some(protocol_coordinator_view(
thread_id.clone(),
loaded,
reorient_state_status,
core_reorient_state_status,
state_revision,
status,
roles,
))
} else {
None
@ -284,16 +298,27 @@ pub async fn map_epiphany_view_response(
ThreadEpiphanyViewResponse {
thread_id: thread_id.clone(),
scene: lenses
.contains(&ThreadEpiphanyViewLens::Scene)
.then(|| map_epiphany_scene(state, loaded, EPIPHANY_REORIENT_LAUNCH_BINDING_ID)),
jobs: if lenses.contains(&ThreadEpiphanyViewLens::Jobs) {
jobs.clone()
scene: lenses.contains(&EpiphanyViewLens::Scene).then(|| {
protocol_scene(derive_epiphany_scene(
state,
loaded,
EPIPHANY_REORIENT_LAUNCH_BINDING_ID,
))
}),
jobs: if lenses.contains(&EpiphanyViewLens::Jobs) {
jobs.iter()
.cloned()
.map(|job| protocol_job_from_surface(job, None, None))
.collect()
} else {
Vec::new()
},
roles: lenses.contains(&ThreadEpiphanyViewLens::Roles).then(|| {
let roles = roles.clone().unwrap_or_default();
roles: lenses.contains(&EpiphanyViewLens::Roles).then(|| {
let role_board = roles.clone();
let protocol_roles = role_board
.as_ref()
.map(protocol_role_board_lanes)
.unwrap_or_default();
ThreadEpiphanyViewRoles {
thread_id: thread_id.clone(),
source: if loaded {
@ -304,18 +329,22 @@ pub async fn map_epiphany_view_response(
state_status: reorient_state_status,
state_revision,
note: render_epiphany_roles_note(
&roles,
reorient_state_status,
role_board
.as_ref()
.map(|role_board| role_board.roles.as_slice())
.unwrap_or(&[]),
core_reorient_state_status,
recommendation
.as_ref()
.map(|recommendation| recommendation.action)
.unwrap_or(ThreadEpiphanyCrrcAction::Continue),
.unwrap_or(epiphany_core::EpiphanyCrrcAction::Continue),
),
roles,
roles: protocol_roles,
}
}),
planning: lenses.contains(&ThreadEpiphanyViewLens::Planning).then(|| {
let (state_status, state_revision, planning, summary) = map_epiphany_planning(state);
planning: lenses.contains(&EpiphanyViewLens::Planning).then(|| {
let (state_status, state_revision, planning, summary) =
protocol_planning_view(derive_epiphany_planning(state));
ThreadEpiphanyViewPlanning {
thread_id: thread_id.clone(),
source: if loaded {
@ -330,11 +359,11 @@ pub async fn map_epiphany_view_response(
}
}),
pressure: lenses
.contains(&ThreadEpiphanyViewLens::Pressure)
.contains(&EpiphanyViewLens::Pressure)
.then(|| pressure.clone())
.flatten(),
reorient: lenses
.contains(&ThreadEpiphanyViewLens::Reorient)
.contains(&EpiphanyViewLens::Reorient)
.then(|| {
reorient_decision
.clone()
@ -352,11 +381,12 @@ pub async fn map_epiphany_view_response(
})
.flatten(),
crrc: lenses
.contains(&ThreadEpiphanyViewLens::Crrc)
.contains(&EpiphanyViewLens::Crrc)
.then(|| {
let pressure = pressure.clone()?;
let decision = reorient_decision.clone()?;
let recommendation = recommendation.clone()?;
let protocol_recommendation = protocol_crrc_recommendation(recommendation.clone());
let available_actions = derive_scene(EpiphanySceneInput {
state,
loaded,
@ -364,7 +394,7 @@ pub async fn map_epiphany_view_response(
})
.available_actions
.into_iter()
.map(map_core_epiphany_scene_action)
.map(protocol_scene_action)
.collect();
let note = format!(
"{} Result status: {:?}. {}",
@ -381,10 +411,12 @@ pub async fn map_epiphany_view_response(
state_revision,
pressure,
decision,
recommendation,
recommendation: protocol_recommendation,
reorient_binding_id: EPIPHANY_REORIENT_LAUNCH_BINDING_ID.to_string(),
reorient_result_status,
reorient_job: reorient_job.clone(),
reorient_job: reorient_job
.clone()
.map(|job| protocol_job_from_surface(job, None, None)),
reorient_finding: reorient_finding.clone(),
available_actions,
note,
@ -392,14 +424,17 @@ pub async fn map_epiphany_view_response(
})
.flatten(),
coordinator: coordinator_response,
lenses,
lenses: lenses
.into_iter()
.map(protocol_view_lens_from_core)
.collect(),
}
}
pub struct EpiphanyRoleResultResponseInput<'a> {
pub thread_id: String,
pub role_id: ThreadEpiphanyRoleId,
pub source: ThreadEpiphanyRolesSource,
pub role_id: EpiphanyRoleResultRoleId,
pub source: EpiphanySurfaceSource,
pub binding_id: String,
pub state: Option<&'a EpiphanyThreadState>,
pub runtime_store_path: Option<&'a Path>,
@ -416,12 +451,16 @@ pub async fn map_epiphany_role_result_response(
state,
runtime_store_path,
} = input;
let protocol_role_id = protocol_role_id_from_core(role_id);
let protocol_source = protocol_roles_source(source);
let Some(state) = state else {
return ThreadEpiphanyRoleResultResponse {
thread_id,
role_id,
source,
state_status: ThreadEpiphanyReorientStateStatus::Missing,
role_id: protocol_role_id,
source: protocol_source,
state_status: protocol_reorient_state_status(
epiphany_core::EpiphanyReorientStateStatus::Missing,
),
state_revision: None,
binding_id,
status: ThreadEpiphanyRoleResultStatus::MissingState,
@ -433,27 +472,33 @@ pub async fn map_epiphany_role_result_response(
let job = map_epiphany_jobs(Some(state), None)
.into_iter()
.find(|job| job.id == binding_id);
let (status, finding, note) =
load_epiphany_role_result_snapshot(state, runtime_store_path, role_id, &binding_id).await;
.find(|job| job.id == binding_id)
.map(|job| protocol_job_from_surface(job, None, None));
let result =
load_core_epiphany_role_result_snapshot(state, runtime_store_path, role_id, &binding_id)
.await;
ThreadEpiphanyRoleResultResponse {
thread_id,
role_id,
source,
state_status: ThreadEpiphanyReorientStateStatus::Ready,
role_id: protocol_role_id,
source: protocol_source,
state_status: protocol_reorient_state_status(
epiphany_core::EpiphanyReorientStateStatus::Ready,
),
state_revision: Some(state.revision),
binding_id,
status,
status: protocol_role_result_status(result.status),
job,
finding,
note,
finding: result
.finding
.map(|finding| protocol_role_finding(protocol_role_id, finding)),
note: result.note,
}
}
pub struct EpiphanyReorientResultResponseInput<'a> {
pub thread_id: String,
pub source: ThreadEpiphanyReorientSource,
pub source: EpiphanySurfaceSource,
pub binding_id: String,
pub state: Option<&'a EpiphanyThreadState>,
pub runtime_store_path: Option<&'a Path>,
@ -469,11 +514,14 @@ pub async fn map_epiphany_reorient_result_response(
state,
runtime_store_path,
} = input;
let protocol_source = protocol_reorient_source(source);
let Some(state) = state else {
return ThreadEpiphanyReorientResultResponse {
thread_id,
source,
state_status: ThreadEpiphanyReorientStateStatus::Missing,
source: protocol_source,
state_status: protocol_reorient_state_status(
epiphany_core::EpiphanyReorientStateStatus::Missing,
),
state_revision: None,
binding_id,
status: ThreadEpiphanyReorientResultStatus::MissingState,
@ -485,45 +533,32 @@ pub async fn map_epiphany_reorient_result_response(
let job = map_epiphany_jobs(Some(state), None)
.into_iter()
.find(|job| job.id == binding_id);
let (status, finding, note) =
load_epiphany_reorient_result_snapshot(Some(state), runtime_store_path, &binding_id).await;
.find(|job| job.id == binding_id)
.map(|job| protocol_job_from_surface(job, None, None));
let result =
load_core_epiphany_reorient_result_snapshot(Some(state), runtime_store_path, &binding_id)
.await;
ThreadEpiphanyReorientResultResponse {
thread_id,
source,
state_status: ThreadEpiphanyReorientStateStatus::Ready,
source: protocol_source,
state_status: protocol_reorient_state_status(
epiphany_core::EpiphanyReorientStateStatus::Ready,
),
state_revision: Some(state.revision),
binding_id,
status,
status: protocol_reorient_result_status(result.status),
job,
finding,
note,
finding: result.finding.map(protocol_reorient_finding),
note: result.note,
}
}
pub fn map_epiphany_distill_response(
expected_revision: u64,
params: ThreadEpiphanyDistillParams,
input: EpiphanyDistillInput,
) -> std::result::Result<ThreadEpiphanyDistillResponse, String> {
let ThreadEpiphanyDistillParams {
source_kind,
status,
text,
subject,
evidence_kind,
code_refs,
..
} = params;
let proposal = distill_observation(EpiphanyDistillInput {
source_kind,
status,
text,
subject,
evidence_kind,
code_refs,
})
.map_err(|err| err.to_string())?;
let proposal = distill_observation(input).map_err(|err| err.to_string())?;
Ok(ThreadEpiphanyDistillResponse {
expected_revision,

View file

@ -13,8 +13,9 @@ path = "src/lib.rs"
anyhow = "1"
bm25 = "2.3.2"
chrono = "0.4.44"
cultcache-rs = { path = "../vendor/cultcache-rs" }
cultnet-rs = { path = "../vendor/cultnet-rs" }
cultcache-rs = { path = "../../CultLib/crates/cultcache-rs" }
cultmesh-rs = { path = "../../CultLib/crates/cultmesh-rs" }
cultnet-rs = { path = "../../CultLib/crates/cultnet-rs" }
epiphany-openai-adapter = { path = "../epiphany-openai-adapter" }
epiphany-state-model = { path = "../epiphany-state-model" }
ignore = "0.4.23"

View file

@ -41,13 +41,11 @@ pub struct EpiphanyAgentMemoryEntry {
#[cultcache(key = 3)]
pub agent: GhostlightAgent,
#[cultcache(key = 4, default)]
pub relationships: Vec<Value>,
pub relationships: Vec<GhostlightRelationship>,
#[cultcache(key = 5, default)]
pub events: Vec<Value>,
pub events: Vec<GhostlightEvent>,
#[cultcache(key = 6, default)]
pub scenes: Vec<Value>,
#[cultcache(key = 7, default)]
pub extra: BTreeMap<String, Value>,
pub scenes: Vec<GhostlightScene>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@ -56,15 +54,11 @@ pub struct GhostlightWorld {
pub setting: String,
pub time: GhostlightTime,
pub canon_context: Vec<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct GhostlightTime {
pub label: String,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@ -74,9 +68,82 @@ pub struct GhostlightAgent {
pub canonical_state: GhostlightCanonicalState,
pub goals: Vec<GhostlightGoal>,
pub memories: GhostlightMemories,
pub perceived_state_overlays: Vec<Value>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
#[serde(default)]
pub perceived_state_overlays: Vec<GhostlightPerceivedStateOverlay>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct GhostlightRelationship {
#[serde(default)]
pub relationship_id: String,
#[serde(default)]
pub participant_ids: Vec<String>,
#[serde(default)]
pub summary: String,
#[serde(default)]
pub stance: String,
#[serde(default)]
pub salience: f64,
#[serde(default)]
pub confidence: f64,
#[serde(default)]
pub linked_memory_ids: Vec<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct GhostlightEvent {
#[serde(default)]
pub event_id: String,
#[serde(default)]
pub kind: String,
#[serde(default)]
pub summary: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp_label: Option<String>,
#[serde(default)]
pub participant_ids: Vec<String>,
#[serde(default)]
pub linked_memory_ids: Vec<String>,
#[serde(default)]
pub salience: f64,
#[serde(default)]
pub confidence: f64,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct GhostlightScene {
#[serde(default)]
pub scene_id: String,
#[serde(default)]
pub label: String,
#[serde(default)]
pub summary: String,
#[serde(default)]
pub participant_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub anchor: Option<String>,
#[serde(default)]
pub salience: f64,
#[serde(default)]
pub status: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct GhostlightPerceivedStateOverlay {
#[serde(default)]
pub overlay_id: String,
#[serde(default)]
pub label: String,
#[serde(default)]
pub summary: String,
#[serde(default)]
pub source: String,
#[serde(default)]
pub salience: f64,
#[serde(default)]
pub confidence: f64,
#[serde(default)]
pub linked_memory_ids: Vec<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@ -87,8 +154,6 @@ pub struct GhostlightIdentity {
pub public_description: String,
#[serde(default)]
pub private_notes: Vec<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@ -100,8 +165,6 @@ pub struct GhostlightCanonicalState {
pub voice_style: BTreeMap<String, GhostlightTraitVector>,
pub situational_state: BTreeMap<String, GhostlightTraitVector>,
pub values: Vec<GhostlightValue>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@ -109,8 +172,6 @@ pub struct GhostlightTraitVector {
pub mean: f64,
pub plasticity: f64,
pub current_activation: f64,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@ -119,8 +180,6 @@ pub struct GhostlightValue {
pub label: String,
pub priority: f64,
pub unforgivable_if_betrayed: bool,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@ -133,8 +192,6 @@ pub struct GhostlightGoal {
#[serde(default)]
pub blockers: Vec<String>,
pub status: String,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@ -142,8 +199,6 @@ pub struct GhostlightMemories {
pub episodic: Vec<GhostlightMemory>,
pub semantic: Vec<GhostlightMemory>,
pub relationship_summaries: Vec<GhostlightMemory>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@ -156,8 +211,6 @@ pub struct GhostlightMemory {
pub linked_event_ids: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub linked_relationship_id: Option<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -166,13 +219,11 @@ struct AgentMemoryProjection {
pub world: GhostlightWorld,
pub agents: Vec<GhostlightAgent>,
#[serde(default)]
pub relationships: Vec<Value>,
pub relationships: Vec<GhostlightRelationship>,
#[serde(default)]
pub events: Vec<Value>,
pub events: Vec<GhostlightEvent>,
#[serde(default)]
pub scenes: Vec<Value>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
pub scenes: Vec<GhostlightScene>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@ -582,7 +633,6 @@ pub fn apply_agent_canonical_trait_seeds(
mean: clamp_unit(seed.mean),
plasticity: clamp_unit(seed.plasticity),
current_activation: clamp_unit(seed.current_activation),
extra: BTreeMap::new(),
};
let mut entry = cache
.get::<EpiphanyAgentMemoryEntry>(&seed.role_id)?
@ -718,7 +768,6 @@ pub fn project_agent_memory_to_json_dir(
relationships: entry.relationships,
events: entry.events,
scenes: entry.scenes,
extra: entry.extra,
};
let path = output_dir.join(filename);
fs::write(
@ -793,7 +842,6 @@ fn entry_from_projection(
relationships: projection.relationships,
events: projection.events,
scenes: projection.scenes,
extra: projection.extra,
})
}
@ -855,6 +903,26 @@ fn validate_agent_entry(entry: &EpiphanyAgentMemoryEntry, expected_agent_id: &st
validate_memory(memory, &format!("memories.{bundle}[{index}]"), &mut errors);
}
}
for (index, overlay) in entry.agent.perceived_state_overlays.iter().enumerate() {
validate_overlay(
overlay,
&format!("perceived_state_overlays[{index}]"),
&mut errors,
);
}
for (index, relationship) in entry.relationships.iter().enumerate() {
validate_relationship(
relationship,
&format!("relationships[{index}]"),
&mut errors,
);
}
for (index, event) in entry.events.iter().enumerate() {
validate_event(event, &format!("events[{index}]"), &mut errors);
}
for (index, scene) in entry.scenes.iter().enumerate() {
validate_scene(scene, &format!("scenes[{index}]"), &mut errors);
}
for (index, goal) in entry.agent.goals.iter().enumerate() {
validate_goal(goal, &format!("goals[{index}]"), &mut errors);
}
@ -905,6 +973,69 @@ fn validate_memory(memory: &GhostlightMemory, path: &str, errors: &mut Vec<Strin
check_unit(memory.confidence, &format!("{path}.confidence"), errors);
}
fn validate_overlay(
overlay: &GhostlightPerceivedStateOverlay,
path: &str,
errors: &mut Vec<String>,
) {
check_optional_identifier(&overlay.overlay_id, &format!("{path}.overlay_id"), errors);
check_string(&overlay.summary, &format!("{path}.summary"), errors, 800);
check_optional_text(&overlay.label, &format!("{path}.label"), errors, 240);
check_optional_text(&overlay.source, &format!("{path}.source"), errors, 240);
check_unit(overlay.salience, &format!("{path}.salience"), errors);
check_unit(overlay.confidence, &format!("{path}.confidence"), errors);
}
fn validate_relationship(
relationship: &GhostlightRelationship,
path: &str,
errors: &mut Vec<String>,
) {
check_optional_identifier(
&relationship.relationship_id,
&format!("{path}.relationship_id"),
errors,
);
check_string(
&relationship.summary,
&format!("{path}.summary"),
errors,
800,
);
check_optional_text(&relationship.stance, &format!("{path}.stance"), errors, 240);
if relationship.participant_ids.is_empty() {
errors.push(format!("{path}.participant_ids must not be empty"));
}
check_unit(relationship.salience, &format!("{path}.salience"), errors);
check_unit(
relationship.confidence,
&format!("{path}.confidence"),
errors,
);
}
fn validate_event(event: &GhostlightEvent, path: &str, errors: &mut Vec<String>) {
check_optional_identifier(&event.event_id, &format!("{path}.event_id"), errors);
check_string(&event.kind, &format!("{path}.kind"), errors, 120);
check_string(&event.summary, &format!("{path}.summary"), errors, 800);
if event.participant_ids.is_empty() {
errors.push(format!("{path}.participant_ids must not be empty"));
}
check_unit(event.salience, &format!("{path}.salience"), errors);
check_unit(event.confidence, &format!("{path}.confidence"), errors);
}
fn validate_scene(scene: &GhostlightScene, path: &str, errors: &mut Vec<String>) {
check_optional_identifier(&scene.scene_id, &format!("{path}.scene_id"), errors);
check_string(&scene.label, &format!("{path}.label"), errors, 240);
check_string(&scene.summary, &format!("{path}.summary"), errors, 800);
if scene.participant_ids.is_empty() {
errors.push(format!("{path}.participant_ids must not be empty"));
}
check_unit(scene.salience, &format!("{path}.salience"), errors);
check_optional_text(&scene.status, &format!("{path}.status"), errors, 120);
}
fn validate_goal(goal: &GhostlightGoal, path: &str, errors: &mut Vec<String>) {
check_string(&goal.goal_id, &format!("{path}.goal_id"), errors, 120);
check_string(
@ -958,6 +1089,29 @@ fn check_string(value: &str, path: &str, errors: &mut Vec<String>, max_len: usiz
}
}
fn check_optional_text(value: &str, path: &str, errors: &mut Vec<String>, max_len: usize) {
if !value.is_empty() && (value.trim().is_empty() || value.len() > max_len) {
errors.push(format!(
"{path} must be empty or text under {max_len} characters"
));
}
}
fn check_optional_identifier(value: &str, path: &str, errors: &mut Vec<String>) {
if value.is_empty() {
return;
}
if value.len() > 120
|| !value.chars().all(|ch| {
ch.is_ascii() && (ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.'))
})
{
errors.push(format!(
"{path} must be empty or an ASCII identifier without whitespace"
));
}
}
fn check_unit(value: f64, path: &str, errors: &mut Vec<String>) {
if !value.is_finite() || !(0.0..=1.0).contains(&value) {
errors.push(format!("{path} must be between 0 and 1"));
@ -1218,7 +1372,6 @@ fn upsert_memories(records: &mut Vec<GhostlightMemory>, incoming: Vec<SelfPatchM
confidence: item.confidence,
linked_event_ids: item.linked_event_ids,
linked_relationship_id: item.linked_relationship_id,
extra: BTreeMap::new(),
},
);
}
@ -1242,7 +1395,6 @@ fn upsert_goals(records: &mut Vec<GhostlightGoal>, incoming: Vec<SelfPatchGoal>)
emotional_stake: item.emotional_stake,
blockers: item.blockers,
status: item.status,
extra: BTreeMap::new(),
},
);
}
@ -1263,7 +1415,6 @@ fn upsert_values(records: &mut Vec<GhostlightValue>, incoming: Vec<SelfPatchValu
label: item.label,
priority: item.priority,
unforgivable_if_betrayed: item.unforgivable_if_betrayed,
extra: BTreeMap::new(),
},
);
}

File diff suppressed because it is too large Load diff

View file

@ -3,10 +3,20 @@ use anyhow::Result;
use anyhow::anyhow;
use chrono::SecondsFormat;
use epiphany_core::EpiphanyAgentMemoryEntry;
use epiphany_core::GhostlightMemory;
use epiphany_core::GhostlightPerceivedStateOverlay;
use epiphany_core::GhostlightTraitVector;
use epiphany_core::HeartbeatAgentReaction;
use epiphany_core::HeartbeatAgentThoughtAppraisal;
use epiphany_core::HeartbeatAppraisalReview;
use epiphany_core::HeartbeatCandidateImplications;
use epiphany_core::HeartbeatEmotionalAppraisal;
use epiphany_core::HeartbeatParticipant;
use epiphany_core::HeartbeatParticipantLocalContext;
use epiphany_core::HeartbeatPersonalityProjection;
use epiphany_core::derive_agent_utterance_state;
use epiphany_core::dossier_profile_for_role;
use epiphany_core::load_agent_memory_entry_for_role;
use serde::Serialize;
use serde_json::Value;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
@ -120,6 +130,9 @@ fn character_turn_packet(
mood: &str,
) -> Value {
let dossier_profile = dossier_profile_for_role(&entry.role_id);
let participant = character_loop_participant(entry, status);
let utterance_state =
derive_agent_utterance_state(entry, Some(&participant), Some(mood), source);
let projection_seed =
deterministic_projection_seed(entry, stimulus, source, mode, status, mood);
let appraisal_seed = deterministic_appraisal_seed(entry, &projection_seed, stimulus, source);
@ -144,6 +157,7 @@ fn character_turn_packet(
"status": status,
"mood": mood,
},
"utteranceState": utterance_state,
"projectedLocalContext": {
"identity": {
"name": entry.agent.identity.name,
@ -172,10 +186,7 @@ fn character_turn_packet(
"priority": value.priority,
"unforgivableIfBetrayed": value.unforgivable_if_betrayed,
})).collect::<Vec<_>>(),
"semanticMemories": entry.agent.memories.semantic.iter().map(memory_projection).collect::<Vec<_>>(),
"episodicMemories": entry.agent.memories.episodic.iter().map(memory_projection).collect::<Vec<_>>(),
"relationshipMemories": entry.agent.memories.relationship_summaries.iter().map(memory_projection).collect::<Vec<_>>(),
"perceivedStateOverlays": entry.agent.perceived_state_overlays.iter().take(6).cloned().collect::<Vec<_>>(),
"perceivedStateOverlays": entry.agent.perceived_state_overlays.iter().take(6).map(perceived_overlay_projection).collect::<Vec<_>>(),
"privateNoteCount": entry.agent.identity.private_notes.len(),
},
"projectionSeed": projection_seed,
@ -243,13 +254,23 @@ fn character_turn_packet(
})
}
fn memory_projection(memory: &GhostlightMemory) -> Value {
serde_json::json!({
"memoryId": memory.memory_id,
"summary": memory.summary,
"salience": memory.salience,
"confidence": memory.confidence,
})
fn character_loop_participant(
entry: &EpiphanyAgentMemoryEntry,
status: &str,
) -> HeartbeatParticipant {
HeartbeatParticipant {
agent_id: entry.agent.agent_id.clone(),
role_id: entry.role_id.clone(),
display_name: entry.agent.identity.name.clone(),
initiative_speed: 1.0,
reaction_bias: 0.5,
interrupt_threshold: 0.5,
status: status.to_string(),
personality_cooldown_multiplier: 1.0,
mood_cooldown_multiplier: 1.0,
initiative_heat_multiplier: 1.0,
..HeartbeatParticipant::default()
}
}
#[derive(Clone, Debug)]
@ -261,6 +282,38 @@ struct TraitSignal {
weight: f64,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct CharacterProjectionSeed {
#[serde(rename = "schema_version")]
schema_version: String,
participant_agent_id: String,
role_id: String,
thought_cluster_ref: String,
thought_tokens: Vec<String>,
dominant_trait_matches: Vec<HeartbeatPersonalityProjection>,
relationship_pressure: CharacterPressureSeed,
perceived_overlay_pressure: CharacterPressureSeed,
contract: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct CharacterPressureSeed {
strength: f64,
summary: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct CharacterReactionSeed {
#[serde(rename = "schema_version")]
schema_version: String,
#[serde(flatten)]
reaction: HeartbeatAgentReaction,
contract: String,
}
fn deterministic_projection_seed(
entry: &EpiphanyAgentMemoryEntry,
stimulus: &str,
@ -268,7 +321,7 @@ fn deterministic_projection_seed(
mode: &str,
status: &str,
mood: &str,
) -> Value {
) -> CharacterProjectionSeed {
let thought_tokens = stimulus_token_set(stimulus, source, mode, status, mood);
let traits = collect_trait_signals(entry);
let mut scored = traits
@ -277,53 +330,50 @@ fn deterministic_projection_seed(
let trait_tokens = summary_tokens(&format!("{} {}", item.group, item.name));
let overlap = token_overlap(&trait_tokens, &thought_tokens);
let projection = round3((item.weight * (0.55 + overlap * 1.8)).clamp(0.0, 1.0));
serde_json::json!({
"group": item.group,
"name": item.name,
"activation": round3(item.activation),
"plasticity": round3(item.plasticity),
"tokenOverlap": round3(overlap),
"projection": projection,
})
HeartbeatPersonalityProjection {
group: item.group.to_string(),
name: item.name.clone(),
activation: round3(item.activation),
plasticity: round3(item.plasticity),
token_overlap: round3(overlap),
projection,
}
})
.collect::<Vec<_>>();
scored.sort_by(|left, right| {
right["projection"]
.as_f64()
.unwrap_or_default()
.total_cmp(&left["projection"].as_f64().unwrap_or_default())
});
scored.sort_by(|left, right| right.projection.total_cmp(&left.projection));
scored.truncate(6);
let (relationship_pressure, relationship_summary) =
strongest_relationship_pressure(entry, &thought_tokens);
let (overlay_pressure, overlay_summary) = strongest_overlay_pressure(entry, &thought_tokens);
serde_json::json!({
"schema_version": "epiphany.personality_projection.v0",
"participantAgentId": entry.agent.agent_id,
"roleId": entry.role_id,
"thoughtClusterRef": format!("{}#stimulus", source),
"thoughtTokens": thought_tokens.into_iter().collect::<Vec<_>>(),
"dominantTraitMatches": scored,
"relationshipPressure": {
"strength": round3(relationship_pressure),
"summary": relationship_summary,
CharacterProjectionSeed {
schema_version: "epiphany.personality_projection.v0".to_string(),
participant_agent_id: entry.agent.agent_id.clone(),
role_id: entry.role_id.clone(),
thought_cluster_ref: format!("{}#stimulus", source),
thought_tokens: thought_tokens.into_iter().collect::<Vec<_>>(),
dominant_trait_matches: scored,
relationship_pressure: CharacterPressureSeed {
strength: round3(relationship_pressure),
summary: relationship_summary,
},
"perceivedOverlayPressure": {
"strength": round3(overlay_pressure),
"summary": overlay_summary,
perceived_overlay_pressure: CharacterPressureSeed {
strength: round3(overlay_pressure),
summary: overlay_summary,
},
"contract": "Projection is participant-local and fallible. It is a seed for embodied response, not reviewed project truth.",
})
contract:
"Projection is participant-local and fallible. It is a seed for embodied response, not reviewed project truth."
.to_string(),
}
}
fn deterministic_appraisal_seed(
entry: &EpiphanyAgentMemoryEntry,
projection_seed: &Value,
projection_seed: &CharacterProjectionSeed,
stimulus: &str,
source: &str,
) -> Value {
) -> HeartbeatAgentThoughtAppraisal {
let traits = collect_trait_signals(entry);
let reactivity = average(traits.iter().take(8).map(|item| item.activation)).unwrap_or(0.5);
let plasticity = average(traits.iter().take(8).map(|item| item.plasticity)).unwrap_or(0.5);
@ -351,17 +401,12 @@ fn deterministic_appraisal_seed(
)
.unwrap_or((1.0 - expressiveness * 0.45).clamp(0.0, 1.0));
let alignment = projection_seed
.pointer("/dominantTraitMatches/0/projection")
.and_then(Value::as_f64)
.dominant_trait_matches
.first()
.map(|item| item.projection)
.unwrap_or(reactivity);
let relationship_pressure = projection_seed
.pointer("/relationshipPressure/strength")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let overlay_pressure = projection_seed
.pointer("/perceivedOverlayPressure/strength")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let relationship_pressure = projection_seed.relationship_pressure.strength;
let overlay_pressure = projection_seed.perceived_overlay_pressure.strength;
let thought_pressure = (0.18
+ (summary_tokens(stimulus).len() as f64 / 18.0).min(0.42)
+ relationship_pressure * 0.22
@ -382,18 +427,17 @@ fn deterministic_appraisal_seed(
round3((arousal * 0.58 + guardedness * 0.22 + thought_pressure * 0.20).clamp(0.0, 1.0));
let valence = round3((0.52 + curiosity * 0.22 - guardedness * 0.20).clamp(0.0, 1.0));
let label = interpretation_label("draft", arousal, guardedness, curiosity);
serde_json::json!({
"schema_version": "epiphany.agent_thought_appraisal.v0",
"appraisalId": format!("face-appraisal-{}", short_id()),
"reviewStatus": "generated_unreviewed",
"participantAgentId": entry.agent.agent_id,
"roleId": entry.role_id,
"currentCharacterStateRef": format!("state/agents.msgpack#{}", entry.role_id),
"thoughtClusterRef": format!("{}#stimulus", source),
"observableThoughtSummary": stimulus,
"participantLocalContext": {
"displayName": entry.agent.identity.name,
"valueLabels": entry
HeartbeatAgentThoughtAppraisal {
schema_version: "epiphany.agent_thought_appraisal.v0".to_string(),
appraisal_id: format!("face-appraisal-{}", short_id()),
review_status: "generated_unreviewed".to_string(),
participant_agent_id: entry.agent.agent_id.clone(),
role_id: entry.role_id.clone(),
current_character_state_ref: format!("state/agents.msgpack#{}", entry.role_id),
thought_cluster_ref: format!("{}#stimulus", source),
participant_local_context: HeartbeatParticipantLocalContext {
display_name: entry.agent.identity.name.clone(),
values: entry
.agent
.canonical_state
.values
@ -401,74 +445,74 @@ fn deterministic_appraisal_seed(
.take(5)
.map(|value| value.label.clone())
.collect::<Vec<_>>(),
"reactivity": round3(reactivity),
"plasticity": round3(plasticity),
"expressiveness": round3(expressiveness),
"guardedness": round3(guarded_baseline),
"relationshipMemoryCount": entry.agent.memories.relationship_summaries.len(),
"perceivedOverlayCount": entry.agent.perceived_state_overlays.len(),
reactivity: round3(reactivity),
plasticity: round3(plasticity),
expressiveness: round3(expressiveness),
guardedness: round3(guarded_baseline),
},
"personalityProjection": projection_seed,
"interpretation": format!(
observable_thought_summary: stimulus.to_string(),
personality_projection: projection_seed.dominant_trait_matches.clone(),
interpretation: format!(
"{} appraises the visible stimulus through its local personality, relationship memory, and perceived overlays; this is embodied response guidance, not omniscient truth.",
entry.agent.identity.name
),
"emotionalAppraisal": {
"valence": valence,
"arousal": arousal,
"urgency": urgency,
"curiosity": curiosity,
"guardedness": guardedness,
"thoughtPressure": round3(thought_pressure),
emotional_appraisal: HeartbeatEmotionalAppraisal {
valence,
arousal,
urgency,
curiosity,
guardedness,
thought_pressure: round3(thought_pressure),
},
"interpretationLabel": label,
"candidateImplications": {
"reactionMode": reaction_mode(&label, "draft"),
"reactionIntensity": round3((urgency * 0.55 + arousal * 0.3 + curiosity * 0.15).clamp(0.0, 1.0)),
"shouldSpeak": entry.role_id == "face" && guardedness < 0.78,
"shouldIncubate": guardedness >= 0.55,
interpretation_label: label.clone(),
confidence_notes: "Deterministic Face-local appraisal rebuilt from the same Ghostlight-style personality projection logic used by heartbeat. Useful for reaction shape, still reviewable and fallible.".to_string(),
candidate_implications: HeartbeatCandidateImplications {
reaction_mode: reaction_mode(&label, "draft").to_string(),
reaction_intensity: round3(
(urgency * 0.55 + arousal * 0.3 + curiosity * 0.15).clamp(0.0, 1.0),
),
should_speak: entry.role_id == "face" && guardedness < 0.78,
should_incubate: guardedness >= 0.55,
},
"confidenceNotes": "Deterministic Face-local appraisal rebuilt from the same Ghostlight-style personality projection logic used by heartbeat. Useful for reaction shape, still reviewable and fallible.",
"review": {
"acceptedForMutation": false,
"rationale": "Appraisal may steer expression and reaction; state mutation still requires explicit review.",
}
})
review: HeartbeatAppraisalReview {
accepted_for_mutation: false,
rationale:
"Appraisal may steer expression and reaction; state mutation still requires explicit review."
.to_string(),
},
}
}
fn deterministic_reaction_seed(role_id: &str, appraisal_seed: &Value) -> Value {
let arousal = appraisal_seed
.pointer("/emotionalAppraisal/arousal")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let guardedness = appraisal_seed
.pointer("/emotionalAppraisal/guardedness")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let curiosity = appraisal_seed
.pointer("/emotionalAppraisal/curiosity")
.and_then(Value::as_f64)
.unwrap_or(0.0);
let mode = appraisal_seed
.pointer("/candidateImplications/reactionMode")
.and_then(Value::as_str)
.unwrap_or("incubate");
let intensity = appraisal_seed
.pointer("/candidateImplications/reactionIntensity")
.and_then(Value::as_f64)
.unwrap_or(0.0);
serde_json::json!({
"schema_version": "epiphany.agent_reaction.v0",
"reactionId": format!("reaction-{}-{}", role_id, short_id()),
"roleId": role_id,
"appraisalId": appraisal_seed.get("appraisalId"),
"mode": mode,
"moodLabel": mood_label(arousal, guardedness, curiosity),
"intensity": round3(intensity),
"surface": if role_id == "face" { "aquarium" } else { "internal" },
"recommendedUse": reaction_recommended_use(role_id, mode),
"contract": "Reaction is derived from local appraisal and may color speech or silence; it still does not mutate durable state by itself.",
})
fn deterministic_reaction_seed(
role_id: &str,
appraisal_seed: &HeartbeatAgentThoughtAppraisal,
) -> CharacterReactionSeed {
let arousal = appraisal_seed.emotional_appraisal.arousal;
let guardedness = appraisal_seed.emotional_appraisal.guardedness;
let curiosity = appraisal_seed.emotional_appraisal.curiosity;
let mode = appraisal_seed.candidate_implications.reaction_mode.as_str();
let intensity = appraisal_seed.candidate_implications.reaction_intensity;
CharacterReactionSeed {
schema_version: "epiphany.agent_reaction.v0".to_string(),
reaction: HeartbeatAgentReaction {
reaction_id: format!("reaction-{}-{}", role_id, short_id()),
role_id: role_id.to_string(),
participant_agent_id: appraisal_seed.participant_agent_id.clone(),
appraisal_id: appraisal_seed.appraisal_id.clone(),
mode: mode.to_string(),
mood_label: mood_label(arousal, guardedness, curiosity).to_string(),
intensity: round3(intensity),
bridge_decision: "draft".to_string(),
surface: if role_id == "face" {
"aquarium"
} else {
"internal"
}
.to_string(),
recommended_use: reaction_recommended_use(role_id, mode).to_string(),
},
contract: "Reaction is derived from local appraisal and may color speech or silence; it still does not mutate durable state by itself.".to_string(),
}
}
fn collect_trait_signals(entry: &EpiphanyAgentMemoryEntry) -> Vec<TraitSignal> {
@ -556,15 +600,10 @@ fn strongest_overlay_pressure(
.perceived_state_overlays
.iter()
.map(|overlay| {
let mut tokens = BTreeSet::new();
collect_json_tokens(overlay, &mut tokens);
let tokens = overlay_tokens(overlay);
let overlap = token_overlap(&tokens, thought_tokens);
let summary = overlay
.get("summary")
.and_then(Value::as_str)
.or_else(|| overlay.get("label").and_then(Value::as_str))
.unwrap_or("Perceived overlay relevance");
(overlap, summary.to_string())
let summary = overlay_summary(overlay);
(overlap, summary)
})
.max_by(|left, right| left.0.total_cmp(&right.0))
.map(|(strength, summary)| (round3(strength.clamp(0.0, 1.0)), summary))
@ -590,21 +629,37 @@ fn stimulus_token_set(
tokens
}
fn collect_json_tokens(value: &Value, tokens: &mut BTreeSet<String>) {
match value {
Value::String(text) => tokens.extend(summary_tokens(text)),
Value::Array(items) => {
for item in items {
collect_json_tokens(item, tokens);
}
}
Value::Object(object) => {
for (key, value) in object {
tokens.extend(summary_tokens(key));
collect_json_tokens(value, tokens);
}
}
_ => {}
fn perceived_overlay_projection(overlay: &GhostlightPerceivedStateOverlay) -> Value {
serde_json::json!({
"overlayId": overlay.overlay_id,
"label": overlay.label,
"summary": overlay.summary,
"source": overlay.source,
"salience": overlay.salience,
"confidence": overlay.confidence,
"linkedMemoryIds": overlay.linked_memory_ids,
})
}
fn overlay_tokens(overlay: &GhostlightPerceivedStateOverlay) -> BTreeSet<String> {
let mut tokens = BTreeSet::new();
tokens.extend(summary_tokens(&overlay.overlay_id));
tokens.extend(summary_tokens(&overlay.label));
tokens.extend(summary_tokens(&overlay.summary));
tokens.extend(summary_tokens(&overlay.source));
for memory_id in &overlay.linked_memory_ids {
tokens.extend(summary_tokens(memory_id));
}
tokens
}
fn overlay_summary(overlay: &GhostlightPerceivedStateOverlay) -> String {
if !overlay.summary.trim().is_empty() {
overlay.summary.clone()
} else if !overlay.label.trim().is_empty() {
overlay.label.clone()
} else {
"Perceived overlay relevance".to_string()
}
}
@ -729,6 +784,24 @@ fn run_smoke() -> Result<Value> {
&& packet["protocol"]["roleId"] == "face"
&& packet["protocol"]["dossierProfile"]["profileKind"] == "embodied_actor"
&& packet["projectedLocalContext"]["dossierProfile"]["profileKind"] == "embodied_actor"
&& packet["utteranceState"]["schemaVersion"] == "epiphany.agent_utterance_state.v0"
&& packet["utteranceState"]["identity"]["name"]
== packet["projectedLocalContext"]["identity"]["name"]
&& packet["utteranceState"]["personalityVectors"]["voiceStyle"].is_object()
&& packet["utteranceState"]["characterStateVector"]["compatibleHandoffSchema"]
== "weksa.utterance_embedding_handoff.v0.1"
&& packet["utteranceState"]["characterStateVector"]["values"]
.as_array()
.is_some_and(|values| values.len() == 64)
&& packet["projectedLocalContext"]
.get("semanticMemories")
.is_none()
&& packet["projectedLocalContext"]
.get("episodicMemories")
.is_none()
&& packet["projectedLocalContext"]
.get("relationshipMemories")
.is_none()
&& packet["projectionSeed"]["schema_version"] == "epiphany.personality_projection.v0"
&& packet["appraisalSeed"]["schema_version"] == "epiphany.agent_thought_appraisal.v0"
&& packet["reactionSeed"]["schema_version"] == "epiphany.agent_reaction.v0"
@ -754,6 +827,7 @@ fn run_smoke() -> Result<Value> {
"projectionSchema": packet["projectionSeed"]["schema_version"],
"appraisalSchema": packet["appraisalSeed"]["schema_version"],
"reactionSchema": packet["reactionSeed"]["schema_version"],
"utteranceStateSchema": packet["utteranceState"]["schemaVersion"],
"identityName": packet["projectedLocalContext"]["identity"]["name"],
"allowedOutputs": packet["allowedOutputs"],
"cognitionLanes": packet["cognitionLanes"]["schema_version"],

View file

@ -0,0 +1,46 @@
use anyhow::Result;
use epiphany_core::EPIPHANY_CULTMESH_INTERNAL_TIER;
use epiphany_core::EPIPHANY_CULTMESH_INTERNAL_VERSE_ID;
use epiphany_core::EPIPHANY_CULTMESH_STATUS_SCHEMA_VERSION;
use epiphany_core::EpiphanyCultMeshStatusEntry;
use epiphany_core::load_epiphany_cultmesh_status;
use epiphany_core::write_epiphany_cultmesh_body_contracts;
use epiphany_core::write_epiphany_cultmesh_eyes_contracts;
use epiphany_core::write_epiphany_cultmesh_global_room_policies;
use epiphany_core::write_epiphany_cultmesh_mind_contracts;
use epiphany_core::write_epiphany_cultmesh_status;
use epiphany_core::write_epiphany_cultmesh_verse_policies;
use std::path::PathBuf;
fn main() -> Result<()> {
let mut store = PathBuf::from(".epiphany-smoke");
store.push("cultmesh");
std::fs::create_dir_all(&store)?;
store.push("epiphany-local.ccmp");
let status = EpiphanyCultMeshStatusEntry {
schema_version: EPIPHANY_CULTMESH_STATUS_SCHEMA_VERSION.to_string(),
runtime_id: "epiphany-cultmesh-smoke".to_string(),
verse_id: EPIPHANY_CULTMESH_INTERNAL_VERSE_ID.to_string(),
verse_tier: EPIPHANY_CULTMESH_INTERNAL_TIER.to_string(),
app_id: "epiphany".to_string(),
note: "Epiphany wrote this through CultMesh, not direct CultNet plumbing.".to_string(),
};
write_epiphany_cultmesh_status(&store, status.clone())?;
write_epiphany_cultmesh_verse_policies(&store, "epiphany-cultmesh-smoke")?;
write_epiphany_cultmesh_global_room_policies(&store, "epiphany-cultmesh-smoke")?;
write_epiphany_cultmesh_mind_contracts(&store, "epiphany-cultmesh-smoke")?;
write_epiphany_cultmesh_body_contracts(&store, "epiphany-cultmesh-smoke")?;
write_epiphany_cultmesh_eyes_contracts(&store, "epiphany-cultmesh-smoke")?;
let loaded = load_epiphany_cultmesh_status(&store, "epiphany-cultmesh-smoke")?;
if loaded != Some(status) {
anyhow::bail!("CultMesh smoke failed to round-trip Epiphany status document");
}
println!(
"epiphany cultmesh smoke ok: {}",
store.canonicalize()?.display()
);
Ok(())
}

View file

@ -3,7 +3,9 @@ use anyhow::Result;
use anyhow::anyhow;
use epiphany_core::GhostlightSceneParticipantSeed;
use epiphany_core::HeartbeatCompleteOptions;
use epiphany_core::HeartbeatHeatUpdateOptions;
use epiphany_core::HeartbeatPumpOptions;
use epiphany_core::HeartbeatQueueMentionOptions;
use epiphany_core::HeartbeatTickOptions;
use epiphany_core::VoidRoutineOptions;
use epiphany_core::apply_agent_self_patch_document;
@ -13,8 +15,10 @@ use epiphany_core::initialize_ghostlight_scene_heartbeat_store;
use epiphany_core::initialize_heartbeat_store;
use epiphany_core::load_heartbeat_state_entry;
use epiphany_core::pump_heartbeat_store;
use epiphany_core::queue_heartbeat_pending_mention_store;
use epiphany_core::run_void_routine_store;
use epiphany_core::tick_heartbeat_store;
use epiphany_core::update_heartbeat_heat_store;
use epiphany_core::validate_agent_memory_store;
use std::env;
use std::fs;
@ -52,6 +56,23 @@ fn main() -> Result<()> {
let mut scene_participants = Vec::<GhostlightSceneParticipantSeed>::new();
let mut source = "epiphany/void-routine".to_string();
let mut no_dream = false;
let mut heat_scope = "global".to_string();
let mut heat_selector = String::new();
let mut heat_multiplier = 1.0_f64;
let mut heat_id: Option<String> = None;
let mut heat_label: Option<String> = None;
let mut heat_reason: Option<String> = None;
let mut heat_expires_after: Option<f64> = None;
let mut heat_clear = false;
let mut channel_id: Option<String> = None;
let mut message_id: Option<String> = None;
let mut author_id: Option<String> = None;
let mut author_name: Option<String> = None;
let mut content: Option<String> = None;
let mut visible_prompt: Option<String> = None;
let mut reply_to_message_id: Option<String> = None;
let mut source_surface = "operator".to_string();
let mut mention_id: Option<String> = None;
while let Some(arg) = args.next() {
match arg.as_str() {
@ -96,6 +117,27 @@ fn main() -> Result<()> {
)?),
"--source" => source = next_value(&mut args, "--source")?,
"--no-dream" => no_dream = true,
"--scope" => heat_scope = next_value(&mut args, "--scope")?,
"--selector" => heat_selector = next_value(&mut args, "--selector")?,
"--multiplier" => heat_multiplier = next_value(&mut args, "--multiplier")?.parse()?,
"--id" => heat_id = Some(next_value(&mut args, "--id")?),
"--label" => heat_label = Some(next_value(&mut args, "--label")?),
"--reason" => heat_reason = Some(next_value(&mut args, "--reason")?),
"--expires-after" => {
heat_expires_after = Some(next_value(&mut args, "--expires-after")?.parse()?)
}
"--clear" => heat_clear = true,
"--channel-id" => channel_id = Some(next_value(&mut args, "--channel-id")?),
"--message-id" => message_id = Some(next_value(&mut args, "--message-id")?),
"--author-id" => author_id = Some(next_value(&mut args, "--author-id")?),
"--author-name" => author_name = Some(next_value(&mut args, "--author-name")?),
"--content" => content = Some(next_value(&mut args, "--content")?),
"--visible-prompt" => visible_prompt = Some(next_value(&mut args, "--visible-prompt")?),
"--reply-to-message-id" => {
reply_to_message_id = Some(next_value(&mut args, "--reply-to-message-id")?)
}
"--source-surface" => source_surface = next_value(&mut args, "--source-surface")?,
"--mention-id" => mention_id = Some(next_value(&mut args, "--mention-id")?),
_ => return Err(anyhow!("unknown argument {arg:?}")),
}
}
@ -207,6 +249,48 @@ fn main() -> Result<()> {
)?;
println!("{}", result);
}
"heat" => {
let store_path = store_path.ok_or_else(|| anyhow!("heat requires --store"))?;
let result = update_heartbeat_heat_store(
&store_path,
HeartbeatHeatUpdateOptions {
scope: heat_scope,
selector: heat_selector,
multiplier: heat_multiplier,
id: heat_id,
label: heat_label,
reason: heat_reason,
expires_after_scene_clock: heat_expires_after,
clear: heat_clear,
},
)?;
println!("{}", result);
}
"queue-mention" => {
let store_path = store_path.ok_or_else(|| anyhow!("queue-mention requires --store"))?;
let target_role_id = role.unwrap_or_else(|| "face".to_string());
let result = queue_heartbeat_pending_mention_store(
&store_path,
HeartbeatQueueMentionOptions {
target_role_id,
source_surface,
channel_id: channel_id
.ok_or_else(|| anyhow!("queue-mention requires --channel-id"))?,
message_id: message_id
.ok_or_else(|| anyhow!("queue-mention requires --message-id"))?,
author_id: author_id
.ok_or_else(|| anyhow!("queue-mention requires --author-id"))?,
author_name,
content: content.ok_or_else(|| anyhow!("queue-mention requires --content"))?,
visible_prompt: visible_prompt
.ok_or_else(|| anyhow!("queue-mention requires --visible-prompt"))?,
reply_to_message_id,
queued_at: None,
mention_id,
},
)?;
println!("{}", result);
}
"status" => {
let store_path = store_path.ok_or_else(|| anyhow!("status requires --store"))?;
if let Some(artifact_dir) = artifact_dir {
@ -285,7 +369,7 @@ fn next_value(args: &mut impl Iterator<Item = String>, name: &str) -> Result<Str
fn usage() -> Result<()> {
Err(anyhow!(
"usage: epiphany-heartbeat-store init --store <path> [--profile epiphany|ghostlight-scene] [--scene-id <id>] [--scene-participant <id|name|speed|reaction|threshold|constraint;constraint>]\n epiphany-heartbeat-store tick --store <path> --artifact-dir <path> [--coordinator-action <action>] [--agent-store <path> --apply-rumination] [--defer-completion]\n epiphany-heartbeat-store pump --store <path> --artifact-dir <path> [--agent-store <path>] [--urgency <0..1>] [--min-heartbeat-rate <n>] [--max-heartbeat-rate <n>] [--min-concurrency <n>] [--max-concurrency <n>] [--max-ticks <n>]\n epiphany-heartbeat-store complete --store <path> --artifact-dir <path> --role <role> [--action-id <id>]\n epiphany-heartbeat-store status --store <path> [--artifact-dir <path>]\n epiphany-heartbeat-store routine --store <path> --artifact-dir <path> [--agent-store <path>] [--source <source>] [--no-dream]\n epiphany-heartbeat-store smoke [--agent-store <path>]"
"usage: epiphany-heartbeat-store init --store <path> [--profile epiphany|ghostlight-scene] [--scene-id <id>] [--scene-participant <id|name|speed|reaction|threshold|constraint;constraint>]\n epiphany-heartbeat-store tick --store <path> --artifact-dir <path> [--coordinator-action <action>] [--agent-store <path> --apply-rumination] [--defer-completion]\n epiphany-heartbeat-store pump --store <path> --artifact-dir <path> [--agent-store <path>] [--urgency <0..1>] [--min-heartbeat-rate <n>] [--max-heartbeat-rate <n>] [--min-concurrency <n>] [--max-concurrency <n>] [--max-ticks <n>]\n epiphany-heartbeat-store heat --store <path> [--scope global|all|agent|role|arena|participant_kind|group] [--selector <id>] [--multiplier <n>] [--id <id>] [--label <text>] [--reason <text>] [--expires-after <scene-clock-delta>] [--clear]\n epiphany-heartbeat-store complete --store <path> --artifact-dir <path> --role <role> [--action-id <id>]\n epiphany-heartbeat-store queue-mention --store <path> [--role face] --channel-id <id> --message-id <id> --author-id <id> --content <text> --visible-prompt <text> [--author-name <name>] [--reply-to-message-id <id>] [--source-surface <name>]\n epiphany-heartbeat-store status --store <path> [--artifact-dir <path>]\n epiphany-heartbeat-store routine --store <path> --artifact-dir <path> [--agent-store <path>] [--source <source>] [--no-dream]\n epiphany-heartbeat-store smoke [--agent-store <path>]"
))
}
@ -335,6 +419,21 @@ fn run_smoke(agent_store: &Path) -> Result<serde_json::Value> {
}
initialize_heartbeat_store(&store_path, 1.0)?;
let heat = update_heartbeat_heat_store(
&store_path,
HeartbeatHeatUpdateOptions {
scope: "role".to_string(),
selector: "implementation".to_string(),
multiplier: 4.0,
id: Some("smoke-implementation-heat".to_string()),
label: Some("Smoke implementation heat".to_string()),
reason: Some(
"Prove heartbeat initiative heat changes the live turn surface.".to_string(),
),
expires_after_scene_clock: Some(20.0),
clear: false,
},
)?;
let work = tick_heartbeat_store(
&store_path,
&artifact_dir,
@ -477,6 +576,21 @@ fn run_smoke(agent_store: &Path) -> Result<serde_json::Value> {
&& idle["rumination"]["result"]["status"] == "accepted"
&& validation_errors.is_empty()
&& initiative_errors.is_empty()
&& heat["heat"]["multipliers"]
.as_array()
.is_some_and(|multipliers| {
multipliers.iter().any(|multiplier| {
multiplier["id"] == "smoke-implementation-heat"
&& multiplier["scope"] == "role"
&& multiplier["selector"] == "implementation"
&& multiplier["multiplier"].as_f64() == Some(4.0)
})
})
&& work["schedule"]["action_catalog"]
.as_array()
.and_then(|actions| actions.first())
.and_then(|action| action["initiative_heat_multiplier"].as_f64())
== Some(4.0)
&& artifact_dir.join("smoke-work.initiative.json").exists()
&& artifact_dir.join("smoke-work.completion.json").exists()
&& artifact_dir.join("smoke-idle.rumination.json").exists()
@ -557,6 +671,7 @@ fn run_smoke(agent_store: &Path) -> Result<serde_json::Value> {
let result = serde_json::json!({
"ok": ok,
"workEvent": work["event"],
"heat": heat["heat"],
"blockedRepeat": blocked_repeat,
"completionEvent": completed["event"],
"idleEvent": idle["event"],
@ -687,6 +802,7 @@ fn validate_schedule_shape(schedule: &serde_json::Value) -> Vec<String> {
"role_work",
"ruminate_memory",
"scene_turn",
"face_turn",
]
.contains(&action["action_type"].as_str().unwrap_or_default())
{
@ -728,6 +844,8 @@ fn validate_schedule_shape(schedule: &serde_json::Value) -> Vec<String> {
"arena",
"participant_kind",
"next_ready_at",
"initiative_frozen",
"freeze_reason",
"reaction_readiness",
"eligible_for_reaction",
]

View file

@ -26,6 +26,8 @@ const SEALED_DIRECT_THOUGHT_KEYS: &[&str] = &[
"inputTranscript",
"activeTranscript",
];
const SEALED_LONG_TEXT_KEYS: &[&str] = &["note"];
const MAX_OPERATOR_TEXT_CHARS: usize = 1200;
fn main() -> Result<()> {
let args = Args::parse()?;
@ -192,7 +194,10 @@ fn run_status(args: &Args) -> Result<Value> {
true,
)?;
let crrc = view.get("crrc").cloned().unwrap_or_else(|| json!(null));
let coordinator = view.get("coordinator").cloned().unwrap_or_else(|| json!(null));
let coordinator = view
.get("coordinator")
.cloned()
.unwrap_or_else(|| json!(null));
let root = env::current_dir().context("failed to resolve current directory")?;
let heartbeat_dir = root.join(".epiphany-heartbeats");
let face_dir = root.join(".epiphany-face");
@ -694,6 +699,13 @@ pub fn sanitize_for_operator(value: Value) -> Value {
.any(|candidate| candidate == &key)
{
sanitized.insert(key.clone(), sealed_direct_thought(&key, &item));
} else if SEALED_LONG_TEXT_KEYS
.iter()
.any(|candidate| candidate == &key)
&& let Some(text) = item.as_str()
&& text.chars().count() > MAX_OPERATOR_TEXT_CHARS
{
sanitized.insert(key.clone(), sealed_long_text(&key, text));
} else {
sanitized.insert(key, sanitize_for_operator(item));
}
@ -723,6 +735,17 @@ fn sealed_direct_thought(key: &str, value: &Value) -> Value {
sealed
}
fn sealed_long_text(key: &str, text: &str) -> Value {
let preview: String = text.chars().take(240).collect();
json!({
"sealed": true,
"key": key,
"reason": "Long operator note sealed to keep status artifacts readable; inspect the sealed transcript or prompt source only during explicit forensic debugging.",
"size": text.chars().count(),
"preview": preview,
})
}
pub fn write_transcript_telemetry(transcript: &Path, output: &Path) -> Result<()> {
let _ = native_json(
"epiphany-agent-telemetry",

View file

@ -7,6 +7,7 @@ use cultcache_rs::DatabaseEntry;
use cultcache_rs::SingleFileMessagePackBackingStore;
use epiphany_core::AgentCanonicalTraitSeed;
use epiphany_core::AgentSelfPatch;
use epiphany_core::HeartbeatBirthPersonalitySeed;
use epiphany_core::apply_agent_canonical_trait_seeds;
use epiphany_core::apply_agent_self_patch_document;
use epiphany_core::default_heartbeat_state;
@ -1540,11 +1541,11 @@ fn process_heartbeat_seed_patches(
};
let previous_initiative_speed = participant.initiative_speed;
let previous_reaction_bias = participant.reaction_bias;
let previous_cooldown = participant
.extra
.get("personalityCooldownMultiplier")
.and_then(Value::as_f64)
.unwrap_or(1.0);
let previous_cooldown = if participant.personality_cooldown_multiplier > 0.0 {
participant.personality_cooldown_multiplier
} else {
1.0
};
let initiative_delta = number_from_map(&seed.heartbeat_deltas, "initiativeSpeedDelta");
let cooldown_delta = number_from_map(&seed.heartbeat_deltas, "cooldownMultiplierDelta");
let urgency = number_from_map(&seed.default_mood_pressure, "urgency");
@ -1554,21 +1555,16 @@ fn process_heartbeat_seed_patches(
participant.reaction_bias =
round3((participant.reaction_bias + urgency * 0.08 + anxiety * 0.04).clamp(0.05, 2.0));
let cooldown = round3((previous_cooldown + cooldown_delta).clamp(0.55, 1.55));
participant
.extra
.insert("personalityCooldownMultiplier".to_string(), json!(cooldown));
participant.extra.insert(
"birthPersonalitySeed".to_string(),
json!({
"schemaVersion": "epiphany.birth_heartbeat_seed.v0",
"source": "repo-personality accept-init",
"projectionId": seed.projection_id,
"repoId": seed.repo_id,
"heartbeatDeltas": seed.heartbeat_deltas,
"defaultMoodPressure": seed.default_mood_pressure,
"contract": "Birth personality may seed heartbeat timing once after Self review; later drift belongs to mood, rumination, sleep, and reviewed selfPatch."
}),
);
participant.personality_cooldown_multiplier = cooldown;
participant.birth_personality_seed = Some(HeartbeatBirthPersonalitySeed {
schema_version: "epiphany.birth_heartbeat_seed.v0".to_string(),
source: "repo-personality accept-init".to_string(),
projection_id: seed.projection_id.clone(),
repo_id: seed.repo_id.clone(),
heartbeat_deltas: seed.heartbeat_deltas.clone(),
default_mood_pressure: seed.default_mood_pressure.clone(),
contract: "Birth personality may seed heartbeat timing once after Self review; later drift belongs to mood, rumination, sleep, and reviewed selfPatch.".to_string(),
});
applied.push(json!({
"roleId": seed.role_id,
"status": "applied",

View file

@ -13,8 +13,8 @@ use epiphany_core::create_runtime_job;
use epiphany_core::create_runtime_session;
use epiphany_core::initialize_runtime_spine;
use epiphany_core::runtime_spine_status;
use epiphany_core::write_runtime_schema_catalog_json;
use epiphany_core::write_runtime_hello_frame;
use epiphany_core::write_runtime_schema_catalog_json;
use std::env;
use std::path::PathBuf;
use uuid::Uuid;

View file

@ -6,7 +6,6 @@ use epiphany_core::add_state_branch;
use epiphany_core::append_state_evidence;
use epiphany_core::close_state_branch;
use epiphany_core::state_ledger_status;
use std::collections::BTreeMap;
use std::env;
use std::path::PathBuf;
@ -29,7 +28,6 @@ fn main() -> Result<()> {
status: require_string_arg(&mut args, "--status")?,
note: require_string_arg(&mut args, "--note")?,
branch: optional_string_arg(&mut args, "--branch")?,
extra: BTreeMap::new(),
};
print_json(&append_state_evidence(store, record)?)?;
}
@ -44,7 +42,6 @@ fn main() -> Result<()> {
status: "active".to_string(),
artifacts,
notes: note.unwrap_or_default(),
extra: BTreeMap::new(),
};
print_json(&add_state_branch(store, branch)?)?;
}

View file

@ -6,7 +6,6 @@ use epiphany_core::add_state_branch;
use epiphany_core::append_state_evidence;
use epiphany_core::close_state_branch;
use epiphany_core::load_state_ledger;
use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::Path;
@ -55,7 +54,6 @@ fn main() -> Result<()> {
status: status.context("missing --status")?,
note: note.context("missing --note")?,
branch,
extra: BTreeMap::new(),
},
)?;
println!("Appended evidence record.");
@ -93,7 +91,6 @@ fn main() -> Result<()> {
status: "active".to_string(),
artifacts,
notes: note.unwrap_or_default(),
extra: BTreeMap::new(),
},
)?;
println!("Added branch '{id}'.");

View file

@ -0,0 +1,127 @@
use serde::Deserialize;
use serde::Serialize;
pub const BODY_REPO_ACCESS_REQUEST_TYPE: &str = "epiphany.body.repo_access_request";
pub const BODY_REPO_ACCESS_REVIEW_TYPE: &str = "epiphany.body.repo_access_review";
pub const BODY_REPO_ACCESS_GRANT_RECEIPT_TYPE: &str = "epiphany.body.repo_access_grant_receipt";
pub const BODY_REPO_ACCESS_REFUSAL_RECEIPT_TYPE: &str = "epiphany.body.repo_access_refusal_receipt";
pub const BODY_REPO_SNAPSHOT_RECEIPT_TYPE: &str = "epiphany.body.repo_snapshot_receipt";
pub const BODY_REPO_MUTATION_RECEIPT_TYPE: &str = "epiphany.body.repo_mutation_receipt";
pub const BODY_REPO_ACCESS_REQUEST_SCHEMA_VERSION: &str = "epiphany.body.repo_access_request.v0";
pub const BODY_REPO_ACCESS_REVIEW_SCHEMA_VERSION: &str = "epiphany.body.repo_access_review.v0";
pub const BODY_REPO_ACCESS_GRANT_RECEIPT_SCHEMA_VERSION: &str =
"epiphany.body.repo_access_grant_receipt.v0";
pub const BODY_REPO_ACCESS_REFUSAL_RECEIPT_SCHEMA_VERSION: &str =
"epiphany.body.repo_access_refusal_receipt.v0";
pub const BODY_REPO_SNAPSHOT_RECEIPT_SCHEMA_VERSION: &str =
"epiphany.body.repo_snapshot_receipt.v0";
pub const BODY_REPO_MUTATION_RECEIPT_SCHEMA_VERSION: &str =
"epiphany.body.repo_mutation_receipt.v0";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BodyCultNetContract {
pub contract_id: String,
pub verse_id: String,
pub document_type: String,
pub payload_schema_version: String,
pub authority: String,
pub operations: Vec<String>,
pub intent_document_types: Vec<String>,
pub receipt_document_types: Vec<String>,
pub notes: Vec<String>,
}
pub fn default_body_cultnet_contracts() -> Vec<BodyCultNetContract> {
vec![
BodyCultNetContract {
contract_id: "epiphany.body.repo_access.review".to_string(),
verse_id: "epiphany-internal".to_string(),
document_type: BODY_REPO_ACCESS_REQUEST_TYPE.to_string(),
payload_schema_version: BODY_REPO_ACCESS_REQUEST_SCHEMA_VERSION.to_string(),
authority: "body".to_string(),
operations: vec![
"intentSubmit".to_string(),
"receiptWatch".to_string(),
"snapshot".to_string(),
],
intent_document_types: vec![BODY_REPO_ACCESS_REQUEST_TYPE.to_string()],
receipt_document_types: vec![
BODY_REPO_ACCESS_REVIEW_TYPE.to_string(),
BODY_REPO_ACCESS_GRANT_RECEIPT_TYPE.to_string(),
BODY_REPO_ACCESS_REFUSAL_RECEIPT_TYPE.to_string(),
],
notes: vec![
"Body is the repo access guardian: workers request repository reads, indexing, commands, edits, and bridge operations here.".to_string(),
"Hands may mutate files only after Body grants scoped access; Eyes may inspect only through scoped Body read/index grants.".to_string(),
],
},
BodyCultNetContract {
contract_id: "epiphany.body.repo_access.review_receipts".to_string(),
verse_id: "epiphany-internal".to_string(),
document_type: BODY_REPO_ACCESS_REVIEW_TYPE.to_string(),
payload_schema_version: BODY_REPO_ACCESS_REVIEW_SCHEMA_VERSION.to_string(),
authority: "readOnly".to_string(),
operations: vec!["snapshot".to_string(), "receiptWatch".to_string()],
intent_document_types: Vec::new(),
receipt_document_types: Vec::new(),
notes: vec![
"Body reviews explain which repo paths, operations, commands, and bridge surfaces were granted or refused.".to_string(),
],
},
BodyCultNetContract {
contract_id: "epiphany.body.repo_snapshot.receipts".to_string(),
verse_id: "epiphany-internal".to_string(),
document_type: BODY_REPO_SNAPSHOT_RECEIPT_TYPE.to_string(),
payload_schema_version: BODY_REPO_SNAPSHOT_RECEIPT_SCHEMA_VERSION.to_string(),
authority: "readOnly".to_string(),
operations: vec!["snapshot".to_string(), "receiptWatch".to_string()],
intent_document_types: Vec::new(),
receipt_document_types: Vec::new(),
notes: vec![
"Repo snapshots are evidence projections from Body access; they do not grant future access by existing.".to_string(),
],
},
BodyCultNetContract {
contract_id: "epiphany.body.repo_mutation.receipts".to_string(),
verse_id: "epiphany-internal".to_string(),
document_type: BODY_REPO_MUTATION_RECEIPT_TYPE.to_string(),
payload_schema_version: BODY_REPO_MUTATION_RECEIPT_SCHEMA_VERSION.to_string(),
authority: "readOnly".to_string(),
operations: vec!["snapshot".to_string(), "receiptWatch".to_string()],
intent_document_types: Vec::new(),
receipt_document_types: Vec::new(),
notes: vec![
"Repo mutation receipts prove Body granted the scoped substrate touch before Hands changed files or ran repo-affecting commands.".to_string(),
],
},
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn body_contracts_make_internal_verse_the_repo_access_gate() {
let contracts = default_body_cultnet_contracts();
let repo_access = contracts
.iter()
.find(|contract| contract.contract_id == "epiphany.body.repo_access.review")
.expect("repo access review contract");
assert_eq!(repo_access.verse_id, "epiphany-internal");
assert_eq!(repo_access.authority, "body");
assert!(
repo_access
.notes
.iter()
.any(|note| note.contains("repo access guardian"))
);
assert!(
repo_access
.receipt_document_types
.contains(&BODY_REPO_ACCESS_GRANT_RECEIPT_TYPE.to_string())
);
}
}

View file

@ -0,0 +1,591 @@
use crate::default_body_cultnet_contracts;
use crate::default_eyes_cultnet_contracts;
use crate::default_mind_cultnet_contracts;
use anyhow::Result;
use cultcache_rs::DatabaseEntry;
use cultmesh_rs::CultMesh;
use cultmesh_rs::CultMeshNode;
use cultmesh_rs::CultMeshNodeOptions;
use cultmesh_rs::cultmesh_documents;
use std::path::Path;
pub const EPIPHANY_CULTMESH_STATUS_TYPE: &str = "epiphany.cultmesh.status";
pub const EPIPHANY_CULTMESH_STATUS_SCHEMA_VERSION: &str = "epiphany.cultmesh.status.v0";
pub const EPIPHANY_CULTMESH_STATUS_KEY: &str = "epiphany-local/status";
pub const EPIPHANY_CULTMESH_VERSE_POLICY_TYPE: &str = "epiphany.cultmesh.verse_policy";
pub const EPIPHANY_CULTMESH_VERSE_POLICY_SCHEMA_VERSION: &str = "epiphany.cultmesh.verse_policy.v0";
pub const EPIPHANY_CULTMESH_GLOBAL_ROOM_POLICY_TYPE: &str = "epiphany.cultmesh.global_room_policy";
pub const EPIPHANY_CULTMESH_GLOBAL_ROOM_POLICY_SCHEMA_VERSION: &str =
"epiphany.cultmesh.global_room_policy.v0";
pub const EPIPHANY_CULTMESH_MIND_CONTRACT_TYPE: &str = "epiphany.cultmesh.mind_contract";
pub const EPIPHANY_CULTMESH_MIND_CONTRACT_SCHEMA_VERSION: &str =
"epiphany.cultmesh.mind_contract.v0";
pub const EPIPHANY_CULTMESH_BODY_CONTRACT_TYPE: &str = "epiphany.cultmesh.body_contract";
pub const EPIPHANY_CULTMESH_BODY_CONTRACT_SCHEMA_VERSION: &str =
"epiphany.cultmesh.body_contract.v0";
pub const EPIPHANY_CULTMESH_EYES_CONTRACT_TYPE: &str = "epiphany.cultmesh.eyes_contract";
pub const EPIPHANY_CULTMESH_EYES_CONTRACT_SCHEMA_VERSION: &str =
"epiphany.cultmesh.eyes_contract.v0";
pub const EPIPHANY_CULTMESH_INTERNAL_VERSE_ID: &str = "epiphany-internal";
pub const EPIPHANY_CULTMESH_LOCAL_AREA_VERSE_ID: &str = "gamecult-local";
pub const EPIPHANY_CULTMESH_GLOBAL_VERSE_ID: &str = "epiphany-global";
pub const EPIPHANY_CULTMESH_INTERNAL_TIER: &str = "internal";
pub const EPIPHANY_CULTMESH_LOCAL_AREA_TIER: &str = "local-area";
pub const EPIPHANY_CULTMESH_GLOBAL_TIER: &str = "global";
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.cultmesh.status",
schema = "EpiphanyCultMeshStatusEntry"
)]
pub struct EpiphanyCultMeshStatusEntry {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub runtime_id: String,
#[cultcache(key = 2)]
pub verse_id: String,
#[cultcache(key = 3)]
pub app_id: String,
#[cultcache(key = 4)]
pub note: String,
#[cultcache(key = 5, default)]
pub verse_tier: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.cultmesh.verse_policy",
schema = "EpiphanyCultMeshVersePolicyEntry"
)]
pub struct EpiphanyCultMeshVersePolicyEntry {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub verse_id: String,
#[cultcache(key = 2)]
pub tier: String,
#[cultcache(key = 3)]
pub purpose: String,
#[cultcache(key = 4)]
pub transport_scope: String,
#[cultcache(key = 5)]
pub trust_boundary: String,
#[cultcache(key = 6)]
pub private_state_allowed: bool,
#[cultcache(key = 7)]
pub untrusted_ingress_allowed: bool,
#[cultcache(key = 8)]
pub yggdrasil_tunnel_allowed: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.cultmesh.global_room_policy",
schema = "EpiphanyCultMeshGlobalRoomPolicyEntry"
)]
pub struct EpiphanyCultMeshGlobalRoomPolicyEntry {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub room_id: String,
#[cultcache(key = 2)]
pub verse_id: String,
#[cultcache(key = 3)]
pub topic: String,
#[cultcache(key = 4)]
pub purpose: String,
#[cultcache(key = 5)]
pub posting_policy: String,
#[cultcache(key = 6)]
pub threaded: bool,
#[cultcache(key = 7)]
pub face_posting_allowed: bool,
#[cultcache(key = 8)]
pub untrusted_ingress_allowed: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.cultmesh.mind_contract",
schema = "EpiphanyCultMeshMindContractEntry"
)]
pub struct EpiphanyCultMeshMindContractEntry {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub contract_id: String,
#[cultcache(key = 2)]
pub verse_id: String,
#[cultcache(key = 3)]
pub document_type: String,
#[cultcache(key = 4)]
pub payload_schema_version: String,
#[cultcache(key = 5)]
pub authority: String,
#[cultcache(key = 6)]
pub operations: Vec<String>,
#[cultcache(key = 7)]
pub intent_document_types: Vec<String>,
#[cultcache(key = 8)]
pub receipt_document_types: Vec<String>,
#[cultcache(key = 9)]
pub notes: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.cultmesh.body_contract",
schema = "EpiphanyCultMeshBodyContractEntry"
)]
pub struct EpiphanyCultMeshBodyContractEntry {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub contract_id: String,
#[cultcache(key = 2)]
pub verse_id: String,
#[cultcache(key = 3)]
pub document_type: String,
#[cultcache(key = 4)]
pub payload_schema_version: String,
#[cultcache(key = 5)]
pub authority: String,
#[cultcache(key = 6)]
pub operations: Vec<String>,
#[cultcache(key = 7)]
pub intent_document_types: Vec<String>,
#[cultcache(key = 8)]
pub receipt_document_types: Vec<String>,
#[cultcache(key = 9)]
pub notes: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.cultmesh.eyes_contract",
schema = "EpiphanyCultMeshEyesContractEntry"
)]
pub struct EpiphanyCultMeshEyesContractEntry {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub contract_id: String,
#[cultcache(key = 2)]
pub verse_id: String,
#[cultcache(key = 3)]
pub document_type: String,
#[cultcache(key = 4)]
pub payload_schema_version: String,
#[cultcache(key = 5)]
pub authority: String,
#[cultcache(key = 6)]
pub operations: Vec<String>,
#[cultcache(key = 7)]
pub intent_document_types: Vec<String>,
#[cultcache(key = 8)]
pub receipt_document_types: Vec<String>,
#[cultcache(key = 9)]
pub notes: Vec<String>,
}
cultmesh_documents!(EpiphanyCultMeshDocuments {
EpiphanyCultMeshStatusEntry => EPIPHANY_CULTMESH_STATUS_SCHEMA_VERSION,
EpiphanyCultMeshVersePolicyEntry => EPIPHANY_CULTMESH_VERSE_POLICY_SCHEMA_VERSION,
EpiphanyCultMeshGlobalRoomPolicyEntry => EPIPHANY_CULTMESH_GLOBAL_ROOM_POLICY_SCHEMA_VERSION,
EpiphanyCultMeshMindContractEntry => EPIPHANY_CULTMESH_MIND_CONTRACT_SCHEMA_VERSION,
EpiphanyCultMeshBodyContractEntry => EPIPHANY_CULTMESH_BODY_CONTRACT_SCHEMA_VERSION,
EpiphanyCultMeshEyesContractEntry => EPIPHANY_CULTMESH_EYES_CONTRACT_SCHEMA_VERSION,
});
pub fn open_epiphany_cultmesh_node(
store_path: impl AsRef<Path>,
runtime_id: impl Into<String>,
) -> Result<CultMeshNode> {
CultMesh::create_node(
store_path,
EpiphanyCultMeshDocuments,
CultMeshNodeOptions {
runtime_id: runtime_id.into(),
..CultMeshNodeOptions::default()
},
)
}
pub fn write_epiphany_cultmesh_status(
store_path: impl AsRef<Path>,
status: EpiphanyCultMeshStatusEntry,
) -> Result<EpiphanyCultMeshStatusEntry> {
let mut node = open_epiphany_cultmesh_node(&store_path, status.runtime_id.clone())?;
let written = node.put(EPIPHANY_CULTMESH_STATUS_KEY, &status)?;
node.flush()?;
Ok(written)
}
pub fn load_epiphany_cultmesh_status(
store_path: impl AsRef<Path>,
runtime_id: impl Into<String>,
) -> Result<Option<EpiphanyCultMeshStatusEntry>> {
let node = open_epiphany_cultmesh_node(store_path, runtime_id)?;
node.get(EPIPHANY_CULTMESH_STATUS_KEY)
}
pub fn epiphany_cultmesh_verse_policies() -> Vec<EpiphanyCultMeshVersePolicyEntry> {
vec![
EpiphanyCultMeshVersePolicyEntry {
schema_version: EPIPHANY_CULTMESH_VERSE_POLICY_SCHEMA_VERSION.to_string(),
verse_id: EPIPHANY_CULTMESH_INTERNAL_VERSE_ID.to_string(),
tier: EPIPHANY_CULTMESH_INTERNAL_TIER.to_string(),
purpose: "Sub-agent typed state: heartbeat, role dossiers, runtime-spine jobs, private receipts, and other Epiphany-owned organs.".to_string(),
transport_scope: "single-host or trusted localhost mesh".to_string(),
trust_boundary: "private Epiphany instance boundary".to_string(),
private_state_allowed: true,
untrusted_ingress_allowed: false,
yggdrasil_tunnel_allowed: false,
},
EpiphanyCultMeshVersePolicyEntry {
schema_version: EPIPHANY_CULTMESH_VERSE_POLICY_SCHEMA_VERSION.to_string(),
verse_id: EPIPHANY_CULTMESH_LOCAL_AREA_VERSE_ID.to_string(),
tier: EPIPHANY_CULTMESH_LOCAL_AREA_TIER.to_string(),
purpose: "Trusted GameCult local-area sharing across projects, including operator-approved tunnels to services on Yggdrasil.".to_string(),
transport_scope: "LAN plus explicit GameCult tunnel endpoints".to_string(),
trust_boundary: "trusted GameCult project/runtime boundary".to_string(),
private_state_allowed: false,
untrusted_ingress_allowed: false,
yggdrasil_tunnel_allowed: true,
},
EpiphanyCultMeshVersePolicyEntry {
schema_version: EPIPHANY_CULTMESH_VERSE_POLICY_SCHEMA_VERSION.to_string(),
verse_id: EPIPHANY_CULTMESH_GLOBAL_VERSE_ID.to_string(),
tier: EPIPHANY_CULTMESH_GLOBAL_TIER.to_string(),
purpose: "Untrusted public surfaces: public dreams, questions, hypotheses, invitations, lineage, ingress receipts, and adoption receipts.".to_string(),
transport_scope: "public internet".to_string(),
trust_boundary: "untrusted public boundary".to_string(),
private_state_allowed: false,
untrusted_ingress_allowed: true,
yggdrasil_tunnel_allowed: false,
},
]
}
pub fn write_epiphany_cultmesh_verse_policies(
store_path: impl AsRef<Path>,
runtime_id: impl Into<String>,
) -> Result<Vec<EpiphanyCultMeshVersePolicyEntry>> {
let mut node = open_epiphany_cultmesh_node(store_path, runtime_id)?;
let mut written = Vec::new();
for policy in epiphany_cultmesh_verse_policies() {
written.push(node.put(policy.verse_id.clone(), &policy)?);
}
node.flush()?;
Ok(written)
}
pub fn epiphany_cultmesh_global_room_policies() -> Vec<EpiphanyCultMeshGlobalRoomPolicyEntry> {
[
(
"dreams",
"Dreams",
"Public dreams, symbolic fragments, imaginative pressure, and unfinished possible worlds.",
),
(
"architecture",
"Architecture",
"System design, ownership maps, protocol boundaries, and rejected machine shapes.",
),
(
"research",
"Research",
"Prior art, papers, source-grounded findings, and scout reports.",
),
(
"faces",
"Faces",
"Public Face identity, voice, social surface, and community-facing presence.",
),
(
"gamecult",
"GameCult",
"GameCult project coordination, public receipts, and cross-project questions.",
),
(
"governance",
"Governance",
"Public proposals and governance-adjacent discussion before any Bifrost adoption.",
),
]
.into_iter()
.map(|(slug, topic, purpose)| EpiphanyCultMeshGlobalRoomPolicyEntry {
schema_version: EPIPHANY_CULTMESH_GLOBAL_ROOM_POLICY_SCHEMA_VERSION.to_string(),
room_id: format!("epiphany-global/{slug}"),
verse_id: EPIPHANY_CULTMESH_GLOBAL_VERSE_ID.to_string(),
topic: topic.to_string(),
purpose: purpose.to_string(),
posting_policy:
"Faces may post public, non-private, citation/provenance-bearing thread roots and replies; local adoption still requires review."
.to_string(),
threaded: true,
face_posting_allowed: true,
untrusted_ingress_allowed: true,
})
.collect()
}
pub fn write_epiphany_cultmesh_global_room_policies(
store_path: impl AsRef<Path>,
runtime_id: impl Into<String>,
) -> Result<Vec<EpiphanyCultMeshGlobalRoomPolicyEntry>> {
let mut node = open_epiphany_cultmesh_node(store_path, runtime_id)?;
let mut written = Vec::new();
for room in epiphany_cultmesh_global_room_policies() {
written.push(node.put(room.room_id.clone(), &room)?);
}
node.flush()?;
Ok(written)
}
pub fn epiphany_cultmesh_mind_contracts() -> Vec<EpiphanyCultMeshMindContractEntry> {
default_mind_cultnet_contracts()
.into_iter()
.map(|contract| EpiphanyCultMeshMindContractEntry {
schema_version: EPIPHANY_CULTMESH_MIND_CONTRACT_SCHEMA_VERSION.to_string(),
contract_id: contract.contract_id,
verse_id: contract.verse_id,
document_type: contract.document_type,
payload_schema_version: contract.payload_schema_version,
authority: contract.authority,
operations: contract.operations,
intent_document_types: contract.intent_document_types,
receipt_document_types: contract.receipt_document_types,
notes: contract.notes,
})
.collect()
}
pub fn write_epiphany_cultmesh_mind_contracts(
store_path: impl AsRef<Path>,
runtime_id: impl Into<String>,
) -> Result<Vec<EpiphanyCultMeshMindContractEntry>> {
let mut node = open_epiphany_cultmesh_node(store_path, runtime_id)?;
let mut written = Vec::new();
for contract in epiphany_cultmesh_mind_contracts() {
written.push(node.put(contract.contract_id.clone(), &contract)?);
}
node.flush()?;
Ok(written)
}
pub fn epiphany_cultmesh_body_contracts() -> Vec<EpiphanyCultMeshBodyContractEntry> {
default_body_cultnet_contracts()
.into_iter()
.map(|contract| EpiphanyCultMeshBodyContractEntry {
schema_version: EPIPHANY_CULTMESH_BODY_CONTRACT_SCHEMA_VERSION.to_string(),
contract_id: contract.contract_id,
verse_id: contract.verse_id,
document_type: contract.document_type,
payload_schema_version: contract.payload_schema_version,
authority: contract.authority,
operations: contract.operations,
intent_document_types: contract.intent_document_types,
receipt_document_types: contract.receipt_document_types,
notes: contract.notes,
})
.collect()
}
pub fn write_epiphany_cultmesh_body_contracts(
store_path: impl AsRef<Path>,
runtime_id: impl Into<String>,
) -> Result<Vec<EpiphanyCultMeshBodyContractEntry>> {
let mut node = open_epiphany_cultmesh_node(store_path, runtime_id)?;
let mut written = Vec::new();
for contract in epiphany_cultmesh_body_contracts() {
written.push(node.put(contract.contract_id.clone(), &contract)?);
}
node.flush()?;
Ok(written)
}
pub fn epiphany_cultmesh_eyes_contracts() -> Vec<EpiphanyCultMeshEyesContractEntry> {
default_eyes_cultnet_contracts()
.into_iter()
.map(|contract| EpiphanyCultMeshEyesContractEntry {
schema_version: EPIPHANY_CULTMESH_EYES_CONTRACT_SCHEMA_VERSION.to_string(),
contract_id: contract.contract_id,
verse_id: contract.verse_id,
document_type: contract.document_type,
payload_schema_version: contract.payload_schema_version,
authority: contract.authority,
operations: contract.operations,
intent_document_types: contract.intent_document_types,
receipt_document_types: contract.receipt_document_types,
notes: contract.notes,
})
.collect()
}
pub fn write_epiphany_cultmesh_eyes_contracts(
store_path: impl AsRef<Path>,
runtime_id: impl Into<String>,
) -> Result<Vec<EpiphanyCultMeshEyesContractEntry>> {
let mut node = open_epiphany_cultmesh_node(store_path, runtime_id)?;
let mut written = Vec::new();
for contract in epiphany_cultmesh_eyes_contracts() {
written.push(node.put(contract.contract_id.clone(), &contract)?);
}
node.flush()?;
Ok(written)
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn epiphany_status_round_trips_through_cultmesh() -> Result<()> {
let temp = tempfile::tempdir()?;
let store = temp.path().join("epiphany-local.ccmp");
let status = EpiphanyCultMeshStatusEntry {
schema_version: EPIPHANY_CULTMESH_STATUS_SCHEMA_VERSION.to_string(),
runtime_id: "epiphany-test".to_string(),
verse_id: EPIPHANY_CULTMESH_INTERNAL_VERSE_ID.to_string(),
verse_tier: EPIPHANY_CULTMESH_INTERNAL_TIER.to_string(),
app_id: "epiphany".to_string(),
note: "CultMesh is the local abstraction over CultCache and CultNet.".to_string(),
};
write_epiphany_cultmesh_status(&store, status.clone())?;
assert_eq!(
load_epiphany_cultmesh_status(&store, "epiphany-test")?,
Some(status)
);
Ok(())
}
#[test]
fn builtin_verse_policies_keep_public_and_private_boundaries_apart() -> Result<()> {
let temp = tempfile::tempdir()?;
let store = temp.path().join("epiphany-verses.ccmp");
let written = write_epiphany_cultmesh_verse_policies(&store, "epiphany-test")?;
assert_eq!(written.len(), 3);
let node = open_epiphany_cultmesh_node(&store, "epiphany-test")?;
let internal = node.get_required::<EpiphanyCultMeshVersePolicyEntry>(
EPIPHANY_CULTMESH_INTERNAL_VERSE_ID,
)?;
let local_area = node.get_required::<EpiphanyCultMeshVersePolicyEntry>(
EPIPHANY_CULTMESH_LOCAL_AREA_VERSE_ID,
)?;
let global = node
.get_required::<EpiphanyCultMeshVersePolicyEntry>(EPIPHANY_CULTMESH_GLOBAL_VERSE_ID)?;
assert!(internal.private_state_allowed);
assert!(!internal.untrusted_ingress_allowed);
assert!(!local_area.private_state_allowed);
assert!(local_area.yggdrasil_tunnel_allowed);
assert!(!global.private_state_allowed);
assert!(global.untrusted_ingress_allowed);
Ok(())
}
#[test]
fn global_room_policies_make_public_threaded_rooms_for_faces() -> Result<()> {
let temp = tempfile::tempdir()?;
let store = temp.path().join("epiphany-global-rooms.ccmp");
let written = write_epiphany_cultmesh_global_room_policies(&store, "epiphany-test")?;
assert!(written.len() >= 5);
let node = open_epiphany_cultmesh_node(&store, "epiphany-test")?;
let dreams =
node.get_required::<EpiphanyCultMeshGlobalRoomPolicyEntry>("epiphany-global/dreams")?;
let architecture = node.get_required::<EpiphanyCultMeshGlobalRoomPolicyEntry>(
"epiphany-global/architecture",
)?;
assert_eq!(dreams.verse_id, EPIPHANY_CULTMESH_GLOBAL_VERSE_ID);
assert!(dreams.threaded);
assert!(dreams.face_posting_allowed);
assert!(dreams.untrusted_ingress_allowed);
assert!(architecture.purpose.contains("ownership"));
Ok(())
}
#[test]
fn mind_contracts_use_verses_to_keep_state_guarded() -> Result<()> {
let temp = tempfile::tempdir()?;
let store = temp.path().join("epiphany-mind-contracts.ccmp");
let written = write_epiphany_cultmesh_mind_contracts(&store, "epiphany-test")?;
assert!(written.len() >= 4);
let node = open_epiphany_cultmesh_node(&store, "epiphany-test")?;
let state_review = node.get_required::<EpiphanyCultMeshMindContractEntry>(
"epiphany.mind.state_effect.review",
)?;
let public_adoption = node.get_required::<EpiphanyCultMeshMindContractEntry>(
"epiphany.mind.public_adoption.review",
)?;
assert_eq!(state_review.verse_id, EPIPHANY_CULTMESH_INTERNAL_VERSE_ID);
assert_eq!(state_review.authority, "mind");
assert!(
state_review
.notes
.iter()
.any(|note| note.contains("persistent state guardian"))
);
assert_eq!(public_adoption.verse_id, EPIPHANY_CULTMESH_GLOBAL_VERSE_ID);
assert!(
public_adoption
.notes
.iter()
.any(|note| note.contains("thought weather"))
);
Ok(())
}
#[test]
fn body_contracts_use_verses_to_keep_repo_access_guarded() -> Result<()> {
let temp = tempfile::tempdir()?;
let store = temp.path().join("epiphany-body-contracts.ccmp");
let written = write_epiphany_cultmesh_body_contracts(&store, "epiphany-test")?;
assert!(written.len() >= 4);
let node = open_epiphany_cultmesh_node(&store, "epiphany-test")?;
let repo_access = node.get_required::<EpiphanyCultMeshBodyContractEntry>(
"epiphany.body.repo_access.review",
)?;
assert_eq!(repo_access.verse_id, EPIPHANY_CULTMESH_INTERNAL_VERSE_ID);
assert_eq!(repo_access.authority, "body");
assert!(
repo_access
.notes
.iter()
.any(|note| note.contains("repo access guardian"))
);
Ok(())
}
#[test]
fn eyes_contracts_use_verses_to_keep_evidence_guarded() -> Result<()> {
let temp = tempfile::tempdir()?;
let store = temp.path().join("epiphany-eyes-contracts.ccmp");
let written = write_epiphany_cultmesh_eyes_contracts(&store, "epiphany-test")?;
assert!(written.len() >= 4);
let node = open_epiphany_cultmesh_node(&store, "epiphany-test")?;
let evidence = node
.get_required::<EpiphanyCultMeshEyesContractEntry>("epiphany.eyes.evidence.review")?;
assert_eq!(evidence.verse_id, EPIPHANY_CULTMESH_INTERNAL_VERSE_ID);
assert_eq!(evidence.authority, "eyes");
assert!(
evidence
.notes
.iter()
.any(|note| note.contains("evidence ingress guardian"))
);
Ok(())
}
}

View file

@ -0,0 +1,124 @@
use serde::Deserialize;
use serde::Serialize;
pub const EYES_EVIDENCE_REQUEST_TYPE: &str = "epiphany.eyes.evidence_request";
pub const EYES_EVIDENCE_REVIEW_TYPE: &str = "epiphany.eyes.evidence_review";
pub const EYES_SOURCE_LOOKUP_RECEIPT_TYPE: &str = "epiphany.eyes.source_lookup_receipt";
pub const EYES_EVIDENCE_PACKET_TYPE: &str = "epiphany.eyes.evidence_packet";
pub const EYES_EVIDENCE_REFUSAL_RECEIPT_TYPE: &str = "epiphany.eyes.evidence_refusal_receipt";
pub const EYES_EVIDENCE_REQUEST_SCHEMA_VERSION: &str = "epiphany.eyes.evidence_request.v0";
pub const EYES_EVIDENCE_REVIEW_SCHEMA_VERSION: &str = "epiphany.eyes.evidence_review.v0";
pub const EYES_SOURCE_LOOKUP_RECEIPT_SCHEMA_VERSION: &str =
"epiphany.eyes.source_lookup_receipt.v0";
pub const EYES_EVIDENCE_PACKET_SCHEMA_VERSION: &str = "epiphany.eyes.evidence_packet.v0";
pub const EYES_EVIDENCE_REFUSAL_RECEIPT_SCHEMA_VERSION: &str =
"epiphany.eyes.evidence_refusal_receipt.v0";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EyesCultNetContract {
pub contract_id: String,
pub verse_id: String,
pub document_type: String,
pub payload_schema_version: String,
pub authority: String,
pub operations: Vec<String>,
pub intent_document_types: Vec<String>,
pub receipt_document_types: Vec<String>,
pub notes: Vec<String>,
}
pub fn default_eyes_cultnet_contracts() -> Vec<EyesCultNetContract> {
vec![
EyesCultNetContract {
contract_id: "epiphany.eyes.evidence.review".to_string(),
verse_id: "epiphany-internal".to_string(),
document_type: EYES_EVIDENCE_REQUEST_TYPE.to_string(),
payload_schema_version: EYES_EVIDENCE_REQUEST_SCHEMA_VERSION.to_string(),
authority: "eyes".to_string(),
operations: vec![
"intentSubmit".to_string(),
"receiptWatch".to_string(),
"snapshot".to_string(),
],
intent_document_types: vec![EYES_EVIDENCE_REQUEST_TYPE.to_string()],
receipt_document_types: vec![
EYES_EVIDENCE_REVIEW_TYPE.to_string(),
EYES_SOURCE_LOOKUP_RECEIPT_TYPE.to_string(),
EYES_EVIDENCE_PACKET_TYPE.to_string(),
EYES_EVIDENCE_REFUSAL_RECEIPT_TYPE.to_string(),
],
notes: vec![
"Eyes is the evidence ingress guardian: organs request source-grounded lookup, provenance, uncertainty, and evidence packets here.".to_string(),
"Body grants substrate access; Eyes turns inspected material into citable evidence packets.".to_string(),
],
},
EyesCultNetContract {
contract_id: "epiphany.eyes.evidence.review_receipts".to_string(),
verse_id: "epiphany-internal".to_string(),
document_type: EYES_EVIDENCE_REVIEW_TYPE.to_string(),
payload_schema_version: EYES_EVIDENCE_REVIEW_SCHEMA_VERSION.to_string(),
authority: "readOnly".to_string(),
operations: vec!["snapshot".to_string(), "receiptWatch".to_string()],
intent_document_types: Vec::new(),
receipt_document_types: Vec::new(),
notes: vec![
"Eyes reviews explain whether a claim has source grounding, needs more looking, or should be refused.".to_string(),
],
},
EyesCultNetContract {
contract_id: "epiphany.eyes.source_lookup.receipts".to_string(),
verse_id: "epiphany-internal".to_string(),
document_type: EYES_SOURCE_LOOKUP_RECEIPT_TYPE.to_string(),
payload_schema_version: EYES_SOURCE_LOOKUP_RECEIPT_SCHEMA_VERSION.to_string(),
authority: "readOnly".to_string(),
operations: vec!["snapshot".to_string(), "receiptWatch".to_string()],
intent_document_types: Vec::new(),
receipt_document_types: Vec::new(),
notes: vec![
"Source lookup receipts prove what was searched or inspected, under which Body grant, before another organ cites it.".to_string(),
],
},
EyesCultNetContract {
contract_id: "epiphany.eyes.evidence_packet.receipts".to_string(),
verse_id: "epiphany-internal".to_string(),
document_type: EYES_EVIDENCE_PACKET_TYPE.to_string(),
payload_schema_version: EYES_EVIDENCE_PACKET_SCHEMA_VERSION.to_string(),
authority: "readOnly".to_string(),
operations: vec!["snapshot".to_string(), "receiptWatch".to_string()],
intent_document_types: Vec::new(),
receipt_document_types: Vec::new(),
notes: vec![
"Evidence packets carry provenance, uncertainty, and source refs for Imagination, Mind, Hands, Soul, Face, Self, Body, and Life.".to_string(),
],
},
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn eyes_contracts_make_internal_verse_the_evidence_gate() {
let contracts = default_eyes_cultnet_contracts();
let evidence = contracts
.iter()
.find(|contract| contract.contract_id == "epiphany.eyes.evidence.review")
.expect("eyes evidence review contract");
assert_eq!(evidence.verse_id, "epiphany-internal");
assert_eq!(evidence.authority, "eyes");
assert!(
evidence
.notes
.iter()
.any(|note| note.contains("evidence ingress guardian"))
);
assert!(
evidence
.receipt_document_types
.contains(&EYES_EVIDENCE_PACKET_TYPE.to_string())
);
}
}

View file

@ -0,0 +1,480 @@
use crate::EpiphanyAgentMemoryEntry;
use crate::EpiphanyOrganDependency;
use crate::HeartbeatPendingMention;
use crate::default_organ_dependencies_for;
use crate::render_organ_dependencies;
use serde::Deserialize;
use serde::Serialize;
pub const FACE_PROJECTOR_PROMPT_SCHEMA_VERSION: &str =
"epiphany.imagination_face_projector_prompt.v0";
pub const FACE_TURN_PROMPT_SCHEMA_VERSION: &str = "epiphany.face_turn_prompt.v0";
pub const FACE_INTERPRETER_PROMPT_SCHEMA_VERSION: &str = "epiphany.face_interpreter_prompt.v0";
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FaceIdentity {
pub identity_id: String,
pub display_name: String,
pub repo_name: String,
#[serde(default)]
pub public_description: String,
#[serde(default)]
pub jurisdiction: Vec<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FaceTranscriptMessage {
pub channel_id: String,
pub message_id: String,
pub author_id: String,
pub author_name: String,
#[serde(default)]
pub is_agent: bool,
pub content: String,
pub timestamp: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FaceRepoActivity {
pub repo_name: String,
pub summary: String,
#[serde(default)]
pub refs: Vec<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FaceSocialAffordance {
pub person_id: String,
pub summary: String,
#[serde(default)]
pub recent_message_ids: Vec<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FaceProjectorInput {
pub identity: FaceIdentity,
#[serde(default)]
pub memory: Option<EpiphanyAgentMemoryEntry>,
#[serde(default)]
pub pending_mentions: Vec<HeartbeatPendingMention>,
#[serde(default)]
pub repo_activity: Vec<FaceRepoActivity>,
#[serde(default)]
pub social_affordances: Vec<FaceSocialAffordance>,
#[serde(default)]
pub organ_dependencies: Vec<EpiphanyOrganDependency>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FaceTurnInput {
pub identity: FaceIdentity,
pub projected_state: String,
#[serde(default)]
pub pending_mentions: Vec<HeartbeatPendingMention>,
#[serde(default)]
pub repo_activity: Vec<FaceRepoActivity>,
#[serde(default)]
pub social_affordances: Vec<FaceSocialAffordance>,
#[serde(default)]
pub transcript: Vec<FaceTranscriptMessage>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FaceInterpreterInput {
pub identity: FaceIdentity,
pub face_prompt: String,
pub face_output: String,
#[serde(default)]
pub pending_mentions: Vec<HeartbeatPendingMention>,
#[serde(default)]
pub allowed_channel_ids: Vec<String>,
}
pub fn build_face_projector_prompt(input: &FaceProjectorInput) -> String {
let memory = input
.memory
.as_ref()
.map(render_memory_packet)
.unwrap_or_else(|| "No durable Face memory entry is loaded.".to_string());
let dependencies = if input.organ_dependencies.is_empty() {
vec![default_organ_dependencies_for("face")]
} else {
input.organ_dependencies.clone()
};
format!(
r#"<!-- prompt:{schema} -->
You are Imagination acting as the Face Projector for {name}.
You are not the Face. You are not Mind. You are the membrane that turns typed role memory, pending social pressure, repo-body activity, relationship affordances, and organ dependencies into lived narrative context.
Hard boundary:
- Do not choose public speech.
- Do not decide durable state; Mind is the Interpreter and state guardian after Face thinks.
- Do not decide repo access; Body gates substrate access before repo facts enter this packet.
- Do not emit action blocks, JSON, state patches, SAY blocks, drafts, or Discord instructions.
- Do not summarize the Face as a job label. Project personhood: values, mood, dignity, pressure, needs, fascinations, wounds, bonds, obligations, fatigue, and what the repo-body motion feels like from inside this Face.
- Project the dependency web. Every sub-agent depends on all the other organs; Face's scene should feel the pressure of Self, Imagination, Eyes, Body, Hands, Soul, and Life without pretending Face owns them.
- If the state is sparse, say what is sparse without inventing history.
Face identity:
{identity}
Typed memory packet:
{memory}
Pending addressed pressure:
{mentions}
Recent home-repo activity:
{activity}
Live social affordances:
{affordances}
Organ dependency contract:
{dependencies}
Return only narrative context for the Face to inhabit.
"#,
schema = FACE_PROJECTOR_PROMPT_SCHEMA_VERSION,
name = input.identity.display_name,
identity = render_identity(&input.identity),
memory = memory,
mentions = render_pending_mentions(&input.pending_mentions),
activity = render_repo_activity(&input.repo_activity),
affordances = render_social_affordances(&input.social_affordances),
dependencies = render_organ_dependencies(&dependencies),
)
}
pub fn build_face_turn_prompt(input: &FaceTurnInput) -> String {
format!(
r#"<!-- prompt:{schema} -->
You are {name}, the Face of {repo}.
Think narratively. Speak, hold silence, wonder, disagree, or form a private thought as yourself.
Hard boundary:
- Do not emit JSON, tool calls, SAY blocks, STATE NOTE blocks, action blocks, or Discord routing syntax.
- You may describe what you want to say or remember in natural language.
- Your side effects are not yours to execute. A parent Interpreter will decide whether your natural turn becomes memory, a draft, public speech, a proposal, or silence.
- Read the raw transcript directly. Recent human correction beats stale memory.
Projected inner state from Imagination:
{projected}
Recent home-repo activity, before room pressure:
{activity}
Pending addressed pressure:
{mentions}
Live social affordances:
{affordances}
Raw room transcript, oldest to newest:
{transcript}
Write one natural Face turn.
"#,
schema = FACE_TURN_PROMPT_SCHEMA_VERSION,
name = input.identity.display_name,
repo = input.identity.repo_name,
projected = input.projected_state.trim(),
activity = render_repo_activity(&input.repo_activity),
mentions = render_pending_mentions(&input.pending_mentions),
affordances = render_social_affordances(&input.social_affordances),
transcript = render_transcript(&input.transcript),
)
}
pub fn build_face_interpreter_prompt(input: &FaceInterpreterInput) -> String {
format!(
r#"<!-- prompt:{schema} -->
You are the parent Face Interpreter for {name}.
You are not the Face. You own the boundary between natural narrative thought and durable side effects.
Hard boundary:
- The Face was forbidden from action syntax. Do not punish natural prose for lacking blocks.
- Decide side effects from the Face output plus the original prompt evidence.
- Public speech must sound like the Face speaking to people, not a scheduler, status report, provenance label, or maintenance note.
- If the Face chooses silence, route without SAY. Preserve useful private pressure as STATE NOTE only when it earns memory.
- Do not auto-post. Emit structured intent for the caller to review or route through the configured Face mouth.
Allowed effect vocabulary:
- STATE NOTE: bounded Face memory, mood, need, social read, bond, value, goal, or agency pressure.
- SAY: public text candidate for an allowed channel.
- DRAFT: private candidate artifact when posting is blocked or needs review.
- ROUTE: non-public action such as keep private, ask Self, or propose a bounded repo investigation.
- DROP: no durable effect.
Allowed channel ids:
{channels}
Pending addressed pressure:
{mentions}
Original Face prompt:
```
{face_prompt}
```
Face output:
```
{face_output}
```
Return concise structured effect blocks. No prose outside the blocks.
"#,
schema = FACE_INTERPRETER_PROMPT_SCHEMA_VERSION,
name = input.identity.display_name,
channels = render_allowed_channels(&input.allowed_channel_ids),
mentions = render_pending_mentions(&input.pending_mentions),
face_prompt = input.face_prompt.trim(),
face_output = input.face_output.trim(),
)
}
pub fn face_projected_surface_is_clean(surface: &str) -> bool {
let forbidden = [
"STATE NOTE",
"SAY:",
"```json",
"\"statePatch\"",
"\"selfPatch\"",
"pending_mentions",
"target_role_id",
"Do not prompt",
];
!forbidden.iter().any(|needle| surface.contains(needle))
}
fn render_identity(identity: &FaceIdentity) -> String {
let jurisdiction = if identity.jurisdiction.is_empty() {
"- No explicit jurisdiction records.".to_string()
} else {
identity
.jurisdiction
.iter()
.map(|item| format!("- {item}"))
.collect::<Vec<_>>()
.join("\n")
};
format!(
"- identity: {}\n- display name: {}\n- repo: {}\n- description: {}\n- jurisdiction:\n{}",
identity.identity_id,
identity.display_name,
identity.repo_name,
fallback(&identity.public_description, "(none)"),
jurisdiction,
)
}
fn render_memory_packet(memory: &EpiphanyAgentMemoryEntry) -> String {
let values = memory
.agent
.canonical_state
.values
.iter()
.map(|value| format!("- {} ({:.2})", value.label, value.priority))
.collect::<Vec<_>>()
.join("\n");
let goals = memory
.agent
.goals
.iter()
.map(|goal| format!("- {} [{}]", goal.description, goal.status))
.collect::<Vec<_>>()
.join("\n");
let notes = memory
.agent
.identity
.private_notes
.iter()
.map(|note| format!("- {note}"))
.collect::<Vec<_>>()
.join("\n");
format!(
"Identity: {}\nPublic description: {}\nPrivate notes:\n{}\nValues:\n{}\nGoals:\n{}",
memory.agent.identity.name,
memory.agent.identity.public_description,
fallback(&notes, "- none"),
fallback(&values, "- none"),
fallback(&goals, "- none"),
)
}
fn render_pending_mentions(mentions: &[HeartbeatPendingMention]) -> String {
if mentions.is_empty() {
return "- none".to_string();
}
mentions
.iter()
.map(|mention| {
format!(
"- {} in channel {} message {}: {}",
mention.author_name.as_deref().unwrap_or(&mention.author_id),
mention.channel_id,
mention.message_id,
mention.visible_prompt
)
})
.collect::<Vec<_>>()
.join("\n")
}
fn render_repo_activity(activity: &[FaceRepoActivity]) -> String {
if activity.is_empty() {
return "- none observed".to_string();
}
activity
.iter()
.map(|item| format!("- {}: {}", item.repo_name, item.summary))
.collect::<Vec<_>>()
.join("\n")
}
fn render_social_affordances(affordances: &[FaceSocialAffordance]) -> String {
if affordances.is_empty() {
return "- none mapped".to_string();
}
affordances
.iter()
.map(|item| format!("- {}: {}", item.person_id, item.summary))
.collect::<Vec<_>>()
.join("\n")
}
fn render_transcript(messages: &[FaceTranscriptMessage]) -> String {
if messages.is_empty() {
return "- room quiet in this packet".to_string();
}
messages
.iter()
.map(|message| {
let agent = if message.is_agent { " (agent)" } else { "" };
format!(
"- [{}] {}{} ({}): {}",
message.timestamp, message.author_name, agent, message.message_id, message.content
)
})
.collect::<Vec<_>>()
.join("\n")
}
fn render_allowed_channels(channel_ids: &[String]) -> String {
if channel_ids.is_empty() {
return "- none configured; SAY must become DRAFT".to_string();
}
channel_ids
.iter()
.map(|channel_id| format!("- {channel_id}"))
.collect::<Vec<_>>()
.join("\n")
}
fn fallback<'a>(value: &'a str, fallback: &'a str) -> &'a str {
if value.trim().is_empty() {
fallback
} else {
value
}
}
#[cfg(test)]
mod tests {
use super::*;
fn identity() -> FaceIdentity {
FaceIdentity {
identity_id: "epiphany".to_string(),
display_name: "Epiphany".to_string(),
repo_name: "EpiphanyAgent".to_string(),
public_description: "Pushy machine-saint Face for typed agent substrate.".to_string(),
jurisdiction: vec!["typed state and review-gated agency".to_string()],
}
}
#[test]
fn face_turn_has_projector_and_interpreter_membranes() {
let pending = HeartbeatPendingMention {
id: "mention-1".to_string(),
target_role_id: "face".to_string(),
target_agent_id: "epiphany.face".to_string(),
source_surface: "discord".to_string(),
channel_id: "aquarium".to_string(),
message_id: "m1".to_string(),
author_id: "human".to_string(),
author_name: Some("Metacrat".to_string()),
content: "Epiphany, report the live cut.".to_string(),
visible_prompt: "report the live cut".to_string(),
reply_to_message_id: None,
queued_at: "2026-05-24T00:00:00+00:00".to_string(),
};
let projector = build_face_projector_prompt(&FaceProjectorInput {
identity: identity(),
pending_mentions: vec![pending.clone()],
repo_activity: vec![FaceRepoActivity {
repo_name: "EpiphanyAgent".to_string(),
summary: "Heartbeat Face membrane is being ported.".to_string(),
refs: vec!["epiphany-core/src/face_turn.rs".to_string()],
}],
..FaceProjectorInput::default()
});
assert!(projector.contains("You are Imagination acting as the Face Projector"));
assert!(projector.contains("You are not the Face"));
assert!(projector.contains("Mind is the Interpreter"));
assert!(projector.contains("Body gates substrate access"));
assert!(projector.contains("Every sub-agent depends on all the other organs"));
assert!(projector.contains("Do not choose public speech"));
let face = build_face_turn_prompt(&FaceTurnInput {
identity: identity(),
projected_state: "Epiphany feels the queue as a direct tug, not a ticket.".to_string(),
pending_mentions: vec![pending.clone()],
transcript: vec![FaceTranscriptMessage {
channel_id: "aquarium".to_string(),
message_id: "m1".to_string(),
author_id: "human".to_string(),
author_name: "Metacrat".to_string(),
is_agent: false,
content: "Epiphany, report the live cut.".to_string(),
timestamp: "2026-05-24T00:00:00+00:00".to_string(),
}],
..FaceTurnInput::default()
});
assert!(face.contains("Think narratively"));
assert!(face.contains("Projected inner state from Imagination"));
assert!(face.contains("Do not emit JSON"));
assert!(face.contains("A parent Interpreter will decide"));
let interpreter = build_face_interpreter_prompt(&FaceInterpreterInput {
identity: identity(),
face_prompt: face,
face_output: "I want to answer, but only if I can name the cut plainly.".to_string(),
pending_mentions: vec![pending],
allowed_channel_ids: vec!["aquarium".to_string()],
});
assert!(interpreter.contains("Allowed effect vocabulary"));
assert!(interpreter.contains("STATE NOTE"));
assert!(interpreter.contains("SAY"));
}
#[test]
fn projected_surface_rejects_side_effect_syntax() {
assert!(face_projected_surface_is_clean(
"Epiphany feels tired, fond, and territorial about clean contracts."
));
assert!(!face_projected_surface_is_clean(
"STATE NOTE: remember this as selfPatch"
));
}
}

File diff suppressed because it is too large Load diff

View file

@ -34,8 +34,14 @@ pub struct EpiphanyHeartbeatStateEntry {
pub participants: Vec<HeartbeatParticipant>,
#[cultcache(key = 6)]
pub history: Vec<HeartbeatHistoryEvent>,
#[cultcache(key = 15, default)]
pub extra: BTreeMap<String, Value>,
#[cultcache(key = 16, default)]
pub initiative_heat: HeartbeatInitiativeHeatPolicy,
#[cultcache(key = 17, default)]
pub protocol: Option<HeartbeatProtocol>,
#[cultcache(key = 18, default)]
pub adaptive_pacing: Option<HeartbeatAdaptivePacing>,
#[cultcache(key = 19, default)]
pub pending_mentions: Vec<HeartbeatPendingMention>,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
@ -55,23 +61,21 @@ pub struct EpiphanyHeartbeatCognitionEntry {
#[cultcache(key = 4, default)]
pub source: Option<String>,
#[cultcache(key = 5, default)]
pub sleep_cycle: Option<Value>,
pub sleep_cycle: Option<HeartbeatSleepCycle>,
#[cultcache(key = 6, default)]
pub memory_resonance: Option<Value>,
pub memory_resonance: Option<HeartbeatMemoryResonance>,
#[cultcache(key = 7, default)]
pub incubation: Option<Value>,
pub incubation: Option<HeartbeatIncubation>,
#[cultcache(key = 8, default)]
pub thought_lanes: Option<Value>,
pub thought_lanes: Option<HeartbeatThoughtLanes>,
#[cultcache(key = 9, default)]
pub bridge: Option<Value>,
pub bridge: Option<HeartbeatCognitionBridge>,
#[cultcache(key = 10, default)]
pub candidate_interventions: Option<Value>,
pub candidate_interventions: Option<HeartbeatCandidateInterventions>,
#[cultcache(key = 11, default)]
pub appraisals: Option<Value>,
pub appraisals: Option<HeartbeatAgentThoughtAppraisals>,
#[cultcache(key = 12, default)]
pub reactions: Option<Value>,
#[cultcache(key = 13, default)]
pub extra: BTreeMap<String, Value>,
pub reactions: Option<HeartbeatAgentReactions>,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
@ -112,6 +116,8 @@ pub(super) struct LegacyHeartbeatStateWithCognition {
pub(super) reactions: Option<Value>,
#[cultcache(key = 15, default)]
pub(super) extra: BTreeMap<String, Value>,
#[cultcache(key = 19, default)]
pub(super) pending_mentions: Vec<HeartbeatPendingMention>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@ -120,8 +126,6 @@ pub struct HeartbeatSelectionPolicy {
pub reaction_precedence: bool,
pub minimum_speed: f64,
pub tie_breakers: Vec<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@ -131,8 +135,468 @@ pub struct HeartbeatPacingPolicy {
pub idle_base_recovery: f64,
pub sleep_heartbeat_rate_multiplier: f64,
pub minimum_effective_rate: f64,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct HeartbeatProtocol {
pub domain: String,
#[serde(default)]
pub scene_id: Option<String>,
pub arena: String,
pub contract: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct HeartbeatAdaptivePacing {
pub schema_version: String,
pub contract: String,
pub pressure: f64,
pub effective_heartbeat_rate: f64,
pub target_concurrency: usize,
pub running_turns: usize,
pub active_participants: usize,
pub signals: HeartbeatAdaptivePacingSignals,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct HeartbeatAdaptivePacingSignals {
pub external_urgency: f64,
pub max_anxiety: f64,
pub average_anxiety: f64,
pub max_urgency: f64,
pub max_arousal: f64,
pub max_thought_pressure: f64,
pub max_reaction_intensity: f64,
pub pending_pressure: f64,
pub contract: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatMemoryResonance {
#[serde(rename = "schema_version")]
pub schema_version: String,
pub updated_at: String,
pub source: String,
pub record_count: usize,
pub pairs: Vec<HeartbeatMemoryResonancePair>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatMemoryResonancePair {
pub left_role: String,
pub left_memory_id: String,
pub left_memory_kind: String,
pub left_summary: String,
pub right_role: String,
pub right_memory_id: String,
pub right_memory_kind: String,
pub right_summary: String,
pub strength: f64,
pub shared_tokens: Vec<String>,
pub source_roles: Vec<String>,
pub source_kinds: Vec<String>,
pub evidence_refs: Vec<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatSourceCoverage {
#[serde(rename = "schema_version")]
pub schema_version: String,
pub updated_at: String,
pub roles: Vec<HeartbeatSourceCoverageRole>,
pub memory_kinds: Vec<HeartbeatSourceCoverageKind>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatSourceCoverageRole {
pub role_id: String,
pub count: usize,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatSourceCoverageKind {
pub kind: String,
pub count: usize,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatIncubation {
#[serde(rename = "schema_version")]
pub schema_version: String,
pub updated_at: String,
pub source_coverage: HeartbeatSourceCoverage,
pub last_incubation_summary: String,
pub themes: Vec<HeartbeatIncubationTheme>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatIncubationTheme {
pub theme_id: String,
pub summary: String,
pub strength: f64,
pub source: String,
pub source_roles: Vec<String>,
pub source_kinds: Vec<String>,
pub source_memory_ids: Vec<String>,
pub support_count: usize,
pub evidence_diversity: f64,
pub exploration_bonus: f64,
pub novelty: f64,
pub novelty_to_self: f64,
pub novelty_to_room: f64,
pub maturation: f64,
pub desire_to_speak: f64,
pub saturation_score: f64,
pub recent_match_count: usize,
pub refractory_penalty: f64,
pub priority_score: f64,
pub status: String,
pub latent_question: String,
pub why_it_pulls: String,
pub holding_close_because: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatThoughtLanes {
#[serde(rename = "schema_version")]
pub schema_version: String,
pub updated_at: String,
pub analytic: HeartbeatAnalyticLane,
pub associative: HeartbeatAssociativeLane,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatAnalyticLane {
pub description: String,
pub active_threads: Vec<HeartbeatAnalyticThread>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatAssociativeLane {
pub description: String,
pub active_threads: Vec<HeartbeatAssociativeThread>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatAnalyticThread {
pub thread_id: String,
pub topic: String,
pub claim: String,
pub evidence_refs: Vec<String>,
pub salience: f64,
pub confidence: f64,
pub desire_to_act: f64,
pub counterweight: String,
pub last_touched_at: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatAssociativeThread {
pub thread_id: String,
pub topic: String,
pub claim: String,
pub source_theme_id: String,
pub novelty: f64,
pub room_relevance: f64,
pub desire_to_speak: f64,
pub status: String,
pub counterweight: String,
pub last_touched_at: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatCognitionBridge {
#[serde(rename = "schema_version")]
pub schema_version: String,
pub updated_at: String,
pub recent_syntheses: Vec<HeartbeatBridgeSynthesis>,
pub source_coverage: HeartbeatSourceCoverage,
pub topic_saturation: Vec<HeartbeatTopicSaturation>,
pub refractory_topics: Vec<HeartbeatRefractoryTopic>,
pub unresolved_tensions: Vec<HeartbeatBridgeTension>,
pub decision: HeartbeatBridgeDecision,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatBridgeSynthesis {
pub timestamp: String,
pub summary: String,
pub dominant_topics: Vec<String>,
pub lane_balance: String,
pub speak_decision: String,
pub theme_status: String,
pub novelty_to_self: f64,
pub novelty_to_room: f64,
pub saturation_score: f64,
pub saturation_note: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatTopicSaturation {
pub topic: String,
pub dominance: f64,
pub recent_mentions: usize,
pub cooling_advice: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatRefractoryTopic {
pub topic: String,
pub penalty: f64,
pub cools_until: String,
pub reason: String,
pub last_triggered_at: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatBridgeTension {
pub topic: String,
pub summary: String,
pub opened_at: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatBridgeDecision {
pub lane_balance: String,
pub speak_decision: String,
pub reason: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatSleepCycle {
#[serde(rename = "schema_version")]
pub schema_version: String,
pub enabled: bool,
pub cycle_hours: i64,
pub nap_duration_minutes: i64,
pub phase_offset_minutes_local: i64,
pub reply_mode: String,
pub is_napping: bool,
pub current_nap_started_at: Option<String>,
pub current_nap_ends_at: Option<String>,
pub next_nap_starts_at: String,
pub last_dream_at: Option<String>,
pub dream_count_in_current_nap: u64,
pub active_dream_themes: Vec<String>,
pub last_distillation_summary: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatAgentThoughtAppraisals {
#[serde(rename = "schema_version")]
pub schema_version: String,
pub updated_at: String,
pub thought_cluster_ref: String,
pub participant_appraisals: Vec<HeartbeatAgentThoughtAppraisal>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatAgentThoughtAppraisal {
#[serde(rename = "schema_version")]
pub schema_version: String,
pub appraisal_id: String,
pub review_status: String,
pub participant_agent_id: String,
pub role_id: String,
pub current_character_state_ref: String,
pub thought_cluster_ref: String,
pub participant_local_context: HeartbeatParticipantLocalContext,
pub observable_thought_summary: String,
pub personality_projection: Vec<HeartbeatPersonalityProjection>,
pub interpretation: String,
pub emotional_appraisal: HeartbeatEmotionalAppraisal,
pub interpretation_label: String,
pub confidence_notes: String,
pub candidate_implications: HeartbeatCandidateImplications,
pub review: HeartbeatAppraisalReview,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatParticipantLocalContext {
pub display_name: String,
pub values: Vec<String>,
pub reactivity: f64,
pub plasticity: f64,
pub expressiveness: f64,
pub guardedness: f64,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatPersonalityProjection {
pub group: String,
pub name: String,
pub activation: f64,
pub plasticity: f64,
pub token_overlap: f64,
pub projection: f64,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatEmotionalAppraisal {
pub valence: f64,
pub arousal: f64,
pub urgency: f64,
pub curiosity: f64,
pub guardedness: f64,
pub thought_pressure: f64,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatCandidateImplications {
pub reaction_mode: String,
pub reaction_intensity: f64,
pub should_speak: bool,
pub should_incubate: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatAppraisalReview {
pub accepted_for_mutation: bool,
pub rationale: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatAgentReactions {
#[serde(rename = "schema_version")]
pub schema_version: String,
pub updated_at: String,
pub reactions: Vec<HeartbeatAgentReaction>,
pub contract: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatAgentReaction {
pub reaction_id: String,
pub role_id: String,
pub participant_agent_id: String,
pub appraisal_id: String,
pub mode: String,
pub mood_label: String,
pub intensity: f64,
pub bridge_decision: String,
pub surface: String,
pub recommended_use: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatCandidateInterventions {
#[serde(rename = "schema_version")]
pub schema_version: String,
pub updated_at: String,
pub items: Vec<HeartbeatCandidateIntervention>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct HeartbeatCandidateIntervention {
pub intervention_id: String,
pub summary: String,
pub draft: String,
pub decision: String,
pub requires_face: bool,
pub requires_review: bool,
pub novelty_to_room: f64,
pub saturation_score: f64,
pub created_at: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct HeartbeatInitiativeHeatPolicy {
#[serde(default = "default_heat_schema_version")]
pub schema_version: String,
#[serde(default = "default_heat_global_multiplier")]
pub global_multiplier: f64,
#[serde(default)]
pub multipliers: Vec<HeartbeatInitiativeMultiplier>,
}
impl Default for HeartbeatInitiativeHeatPolicy {
fn default() -> Self {
Self {
schema_version: default_heat_schema_version(),
global_multiplier: default_heat_global_multiplier(),
multipliers: Vec::new(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct HeartbeatInitiativeMultiplier {
pub id: String,
#[serde(default)]
pub label: String,
#[serde(default = "default_heat_scope")]
pub scope: String,
#[serde(default)]
pub selector: String,
#[serde(default = "default_heat_global_multiplier")]
pub multiplier: f64,
#[serde(default)]
pub reason: String,
#[serde(default)]
pub updated_at: Option<String>,
#[serde(default)]
pub expires_at_scene_clock: Option<f64>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@ -155,8 +619,89 @@ pub struct HeartbeatParticipant {
pub last_woke_at: Option<String>,
pub last_finished_at: Option<String>,
pub pending_turn: Option<HeartbeatPendingTurn>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
#[serde(default)]
pub scene_id: Option<String>,
#[serde(default)]
pub groups: Vec<String>,
#[serde(default = "default_heat_global_multiplier")]
pub personality_cooldown_multiplier: f64,
#[serde(default = "default_heat_global_multiplier")]
pub mood_cooldown_multiplier: f64,
#[serde(default = "default_heat_global_multiplier")]
pub initiative_heat_multiplier: f64,
#[serde(default)]
pub initiative_heat: Option<HeartbeatInitiativeHeatProjection>,
#[serde(default)]
pub personality_timing: Option<HeartbeatPersonalityTiming>,
#[serde(default)]
pub mood_timing: Option<HeartbeatMoodTiming>,
#[serde(default)]
pub birth_personality_seed: Option<HeartbeatBirthPersonalitySeed>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct HeartbeatInitiativeHeatProjection {
pub schema_version: String,
pub global_multiplier: f64,
pub effective_multiplier: f64,
pub basis: Vec<HeartbeatInitiativeHeatBasis>,
pub contract: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct HeartbeatInitiativeHeatBasis {
pub id: String,
pub scope: String,
pub selector: String,
pub multiplier: f64,
pub reason: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct HeartbeatPersonalityTiming {
pub schema_version: String,
pub source: String,
pub cooldown_multiplier: f64,
pub work_drive: f64,
pub handsiness: f64,
pub caution: f64,
pub rumination_bias: f64,
pub basis: Vec<String>,
pub contract: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct HeartbeatMoodTiming {
pub schema_version: String,
pub source: Option<String>,
pub cooldown_multiplier: f64,
#[serde(default)]
pub emotional_dimensions: Vec<HeartbeatMoodDimension>,
pub anxiety: f64,
pub urgency: f64,
pub arousal: f64,
pub thought_pressure: f64,
pub guardedness: f64,
pub reaction_intensity: f64,
pub contract: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct HeartbeatMoodDimension {
pub name: String,
pub value: f64,
pub source_path: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct HeartbeatBirthPersonalitySeed {
pub schema_version: String,
pub source: String,
pub projection_id: String,
pub repo_id: String,
pub heartbeat_deltas: BTreeMap<String, f64>,
pub default_mood_pressure: BTreeMap<String, f64>,
pub contract: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@ -187,8 +732,49 @@ pub struct HeartbeatPendingTurn {
pub completed_scene_clock: Option<f64>,
#[serde(rename = "nextReadyAt", default)]
pub next_ready_at: Option<f64>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
#[serde(
rename = "personalityCooldownMultiplier",
default = "default_heat_global_multiplier"
)]
pub personality_cooldown_multiplier: f64,
#[serde(
rename = "moodCooldownMultiplier",
default = "default_heat_global_multiplier"
)]
pub mood_cooldown_multiplier: f64,
#[serde(
rename = "initiativeHeatMultiplier",
default = "default_heat_global_multiplier"
)]
pub initiative_heat_multiplier: f64,
#[serde(
rename = "effectiveCooldownMultiplier",
default = "default_heat_global_multiplier"
)]
pub effective_cooldown_multiplier: f64,
#[serde(rename = "initiativeFrozen", default)]
pub initiative_frozen: bool,
#[serde(rename = "initiativeFreezeReason", default)]
pub initiative_freeze_reason: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HeartbeatPendingMention {
pub id: String,
pub target_role_id: String,
pub target_agent_id: String,
pub source_surface: String,
pub channel_id: String,
pub message_id: String,
pub author_id: String,
#[serde(default)]
pub author_name: Option<String>,
pub content: String,
pub visible_prompt: String,
#[serde(default)]
pub reply_to_message_id: Option<String>,
pub queued_at: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@ -224,8 +810,6 @@ pub struct HeartbeatHistoryEvent {
pub turn_status: Option<String>,
#[serde(rename = "cooldownStartedAfterCompletion", default)]
pub cooldown_started_after_completion: Option<bool>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq)]
@ -256,6 +840,33 @@ pub struct HeartbeatPumpOptions {
pub agent_store: Option<std::path::PathBuf>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct HeartbeatHeatUpdateOptions {
pub scope: String,
pub selector: String,
pub multiplier: f64,
pub id: Option<String>,
pub label: Option<String>,
pub reason: Option<String>,
pub expires_after_scene_clock: Option<f64>,
pub clear: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct HeartbeatQueueMentionOptions {
pub target_role_id: String,
pub source_surface: String,
pub channel_id: String,
pub message_id: String,
pub author_id: String,
pub author_name: Option<String>,
pub content: String,
pub visible_prompt: String,
pub reply_to_message_id: Option<String>,
pub queued_at: Option<String>,
pub mention_id: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct HeartbeatCompleteOptions {
pub role: String,
@ -278,3 +889,15 @@ pub struct GhostlightSceneParticipantSeed {
pub interrupt_threshold: f64,
pub constraints: Vec<String>,
}
fn default_heat_schema_version() -> String {
"epiphany.heartbeat_initiative_heat.v0".to_string()
}
fn default_heat_global_multiplier() -> f64 {
1.0
}
fn default_heat_scope() -> String {
"agent".to_string()
}

View file

@ -1,9 +1,12 @@
use super::EpiphanyHeartbeatStateEntry;
use super::HeartbeatAdaptivePacingSignals;
use super::HeartbeatInitiativeHeatBasis;
use super::HeartbeatInitiativeHeatProjection;
use super::HeartbeatInitiativeMultiplier;
use super::HeartbeatParticipant;
use super::HeartbeatPumpOptions;
use super::round3;
use super::round6;
use serde_json::Value;
#[derive(Clone, Debug)]
pub(super) struct AdaptiveSwarmPacing {
@ -12,7 +15,7 @@ pub(super) struct AdaptiveSwarmPacing {
pub(super) target_concurrency: usize,
pub(super) running_turns: usize,
pub(super) active_participants: usize,
pub(super) signals: Value,
pub(super) signals: HeartbeatAdaptivePacingSignals,
}
pub(super) fn adaptive_swarm_pacing(
@ -74,17 +77,17 @@ pub(super) fn adaptive_swarm_pacing(
target_concurrency,
running_turns,
active_participants,
signals: serde_json::json!({
"externalUrgency": round3(external_urgency),
"maxAnxiety": round3(mood_signals.max_anxiety),
"averageAnxiety": round3(mood_signals.average_anxiety),
"maxUrgency": round3(mood_signals.max_urgency),
"maxArousal": round3(mood_signals.max_arousal),
"maxThoughtPressure": round3(mood_signals.max_thought_pressure),
"maxReactionIntensity": round3(mood_signals.max_reaction_intensity),
"pendingPressure": round3(pending_pressure),
"contract": "Anxiety-like state raises tempo and concurrency; calm state lets the swarm sleep slow.",
}),
signals: HeartbeatAdaptivePacingSignals {
external_urgency: round3(external_urgency),
max_anxiety: round3(mood_signals.max_anxiety),
average_anxiety: round3(mood_signals.average_anxiety),
max_urgency: round3(mood_signals.max_urgency),
max_arousal: round3(mood_signals.max_arousal),
max_thought_pressure: round3(mood_signals.max_thought_pressure),
max_reaction_intensity: round3(mood_signals.max_reaction_intensity),
pending_pressure: round3(pending_pressure),
contract: "Anxiety-like state raises tempo and concurrency; calm state lets the swarm sleep slow.".to_string(),
},
}
}
@ -103,14 +106,14 @@ fn swarm_mood_signals(state: &EpiphanyHeartbeatStateEntry) -> SwarmMoodSignals {
let mut anxiety_total = 0.0;
let mut anxiety_count = 0_usize;
for participant in &state.participants {
let Some(mood) = participant.extra.get("moodTiming") else {
let Some(mood) = participant.mood_timing.as_ref() else {
continue;
};
let anxiety = number_at(mood, "/anxiety");
let urgency = number_at(mood, "/urgency");
let arousal = number_at(mood, "/arousal");
let thought_pressure = number_at(mood, "/thoughtPressure");
let reaction_intensity = number_at(mood, "/reactionIntensity");
let anxiety = mood.anxiety;
let urgency = mood.urgency;
let arousal = mood.arousal;
let thought_pressure = mood.thought_pressure;
let reaction_intensity = mood.reaction_intensity;
signals.max_anxiety = signals.max_anxiety.max(anxiety);
signals.max_urgency = signals.max_urgency.max(urgency);
signals.max_arousal = signals.max_arousal.max(arousal);
@ -138,34 +141,102 @@ pub(super) fn running_turn_count(state: &EpiphanyHeartbeatStateEntry) -> usize {
.count()
}
fn number_at(value: &Value, pointer: &str) -> f64 {
value
.pointer(pointer)
.and_then(Value::as_f64)
.unwrap_or(0.0)
}
pub(super) fn personality_cooldown_multiplier(participant: &HeartbeatParticipant) -> f64 {
participant
.extra
.get("personalityCooldownMultiplier")
.and_then(Value::as_f64)
.unwrap_or(1.0)
.clamp(0.25, 3.0)
nonzero_multiplier(participant.personality_cooldown_multiplier).clamp(0.25, 3.0)
}
pub(super) fn mood_cooldown_multiplier(participant: &HeartbeatParticipant) -> f64 {
participant
.extra
.get("moodCooldownMultiplier")
.and_then(Value::as_f64)
.unwrap_or(1.0)
.clamp(0.25, 3.0)
nonzero_multiplier(participant.mood_cooldown_multiplier).clamp(0.25, 3.0)
}
pub(super) fn effective_cooldown_multiplier(participant: &HeartbeatParticipant) -> f64 {
round3(personality_cooldown_multiplier(participant) * mood_cooldown_multiplier(participant))
.clamp(0.20, 4.0)
round3(
personality_cooldown_multiplier(participant) * mood_cooldown_multiplier(participant)
/ initiative_heat_multiplier(participant),
)
.clamp(0.05, 8.0)
}
pub(super) fn initiative_heat_multiplier(participant: &HeartbeatParticipant) -> f64 {
nonzero_multiplier(participant.initiative_heat_multiplier).clamp(0.05, 25.0)
}
fn nonzero_multiplier(value: f64) -> f64 {
if value.is_finite() && value > 0.0 {
value
} else {
1.0
}
}
pub(super) fn apply_initiative_heat_policy(state: &mut EpiphanyHeartbeatStateEntry) {
let global = state.initiative_heat.global_multiplier.clamp(0.05, 25.0);
let active_multipliers = state
.initiative_heat
.multipliers
.iter()
.filter(|multiplier| {
multiplier
.expires_at_scene_clock
.is_none_or(|expires| expires > state.scene_clock)
})
.cloned()
.collect::<Vec<_>>();
for participant in &mut state.participants {
let mut heat = global;
let mut basis = Vec::<HeartbeatInitiativeHeatBasis>::new();
for multiplier in &active_multipliers {
if multiplier_matches_participant(multiplier, participant) {
let value = multiplier.multiplier.clamp(0.05, 25.0);
heat *= value;
basis.push(HeartbeatInitiativeHeatBasis {
id: multiplier.id.clone(),
scope: multiplier.scope.clone(),
selector: multiplier.selector.clone(),
multiplier: value,
reason: multiplier.reason.clone(),
});
}
}
let heat = round3(heat.clamp(0.05, 25.0));
participant.initiative_heat_multiplier = heat;
participant.initiative_heat = Some(HeartbeatInitiativeHeatProjection {
schema_version: "epiphany.heartbeat_initiative_heat_projection.v0".to_string(),
global_multiplier: global,
effective_multiplier: heat,
basis,
contract: "Initiative heat accelerates turn recovery. Proximity or agency pressure may raise heat, but the typed heat policy remains the authority.".to_string(),
});
}
}
fn multiplier_matches_participant(
multiplier: &HeartbeatInitiativeMultiplier,
participant: &HeartbeatParticipant,
) -> bool {
let selector = multiplier.selector.trim();
match multiplier.scope.as_str() {
"all" => true,
"agent" | "agent_id" => eq_key(&participant.agent_id, selector),
"role" | "role_id" => eq_key(&participant.role_id, selector),
"arena" => eq_key(participant.arena.as_str(), selector),
"participant_kind" | "kind" => eq_key(participant.participant_kind.as_str(), selector),
"group" | "constraint" => {
participant
.constraints
.iter()
.any(|constraint| eq_key(constraint, selector))
|| participant
.groups
.iter()
.any(|group| eq_key(group, selector))
}
_ => false,
}
}
fn eq_key(left: &str, right: &str) -> bool {
left.trim().eq_ignore_ascii_case(right.trim())
}
#[cfg(test)]
@ -177,17 +248,55 @@ mod tests {
#[test]
fn running_turn_count_reads_only_running_pending_turns() {
let mut state = default_heartbeat_state(1.0);
state.participants[0].pending_turn =
Some(crate::heartbeat_state::HeartbeatPendingTurn {
status: "running".to_string(),
..Default::default()
});
state.participants[1].pending_turn =
Some(crate::heartbeat_state::HeartbeatPendingTurn {
status: "completed".to_string(),
..Default::default()
});
state.participants[0].pending_turn = Some(crate::heartbeat_state::HeartbeatPendingTurn {
status: "running".to_string(),
..Default::default()
});
state.participants[1].pending_turn = Some(crate::heartbeat_state::HeartbeatPendingTurn {
status: "completed".to_string(),
..Default::default()
});
assert_eq!(running_turn_count(&state), 1);
}
#[test]
fn initiative_heat_multiplies_agent_and_group_recovery_speed() {
let mut state = default_heartbeat_state(1.0);
state.initiative_heat.global_multiplier = 2.0;
state.initiative_heat.multipliers = vec![
HeartbeatInitiativeMultiplier {
id: "hands".to_string(),
scope: "role".to_string(),
selector: "implementation".to_string(),
multiplier: 3.0,
..Default::default()
},
HeartbeatInitiativeMultiplier {
id: "all-maintenance".to_string(),
scope: "arena".to_string(),
selector: "maintenance".to_string(),
multiplier: 1.5,
..Default::default()
},
];
apply_initiative_heat_policy(&mut state);
let implementation = state
.participants
.iter()
.find(|participant| participant.role_id == "implementation")
.expect("implementation participant");
let research = state
.participants
.iter()
.find(|participant| participant.role_id == "research")
.expect("research participant");
assert_eq!(initiative_heat_multiplier(implementation), 9.0);
assert_eq!(initiative_heat_multiplier(research), 3.0);
assert!(
effective_cooldown_multiplier(implementation) < effective_cooldown_multiplier(research)
);
}
}

View file

@ -9,6 +9,7 @@ use super::HeartbeatPendingTurn;
use super::HeartbeatSelectionPolicy;
use super::VOID_ROUTINE_SCHEMA_VERSION;
use super::effective_cooldown_multiplier;
use super::initiative_heat_multiplier;
use super::load_heartbeat_cognition_entry;
use super::load_heartbeat_state_entry;
use super::mood_cooldown_multiplier;
@ -68,6 +69,22 @@ pub fn heartbeat_status_projection(
"artifactDir": artifact_dir.as_ref(),
"targetHeartbeatRate": state.target_heartbeat_rate,
"sceneClock": state.scene_clock,
"initiativeHeat": {
"schemaVersion": state.initiative_heat.schema_version,
"globalMultiplier": state.initiative_heat.global_multiplier,
"multipliers": state.initiative_heat.multipliers.iter().map(|multiplier| {
serde_json::json!({
"id": multiplier.id,
"label": multiplier.label,
"scope": multiplier.scope,
"selector": multiplier.selector,
"multiplier": multiplier.multiplier,
"reason": multiplier.reason,
"updatedAt": multiplier.updated_at,
"expiresAtSceneClock": multiplier.expires_at_scene_clock,
})
}).collect::<Vec<_>>(),
},
"participants": state.participants.iter().map(participant_status_json).collect::<Vec<_>>(),
"latestEvent": history.last().cloned(),
"history": history,
@ -81,8 +98,8 @@ pub fn heartbeat_status_projection(
"candidateInterventions": cognition.as_ref().and_then(|entry| entry.candidate_interventions.clone()),
"appraisals": cognition.as_ref().and_then(|entry| entry.appraisals.clone()),
"reactions": cognition.as_ref().and_then(|entry| entry.reactions.clone()),
"adaptivePacing": state.extra.get("adaptivePacing"),
"availableActions": ["init", "tick", "pump", "complete", "status", "routine"],
"adaptivePacing": state.adaptive_pacing.as_ref(),
"availableActions": ["init", "tick", "pump", "heat", "complete", "status", "routine"],
}))
}
@ -94,7 +111,10 @@ fn latest_json_artifacts(artifact_dir: impl AsRef<Path>, limit: usize) -> Vec<Va
let mut paths: Vec<_> = read_dir
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| path.extension().is_some_and(|extension| extension == "json"))
.filter(|path| {
path.extension()
.is_some_and(|extension| extension == "json")
})
.filter_map(|path| {
let modified = path.metadata().and_then(|meta| meta.modified()).ok()?;
Some((path, modified))
@ -121,7 +141,8 @@ fn latest_json_artifacts(artifact_dir: impl AsRef<Path>, limit: usize) -> Vec<Va
}
fn artifact_summary(payload: &Value) -> Value {
if payload.get("schema_version") == Some(&Value::String(VOID_ROUTINE_SCHEMA_VERSION.to_string()))
if payload.get("schema_version")
== Some(&Value::String(VOID_ROUTINE_SCHEMA_VERSION.to_string()))
{
return serde_json::json!({
"runId": payload.get("runId"),
@ -203,9 +224,19 @@ fn participant_status_json(participant: &HeartbeatParticipant) -> Value {
"initiativeSpeed": participant.initiative_speed,
"personalityCooldownMultiplier": personality_cooldown_multiplier(participant),
"moodCooldownMultiplier": mood_cooldown_multiplier(participant),
"initiativeHeatMultiplier": initiative_heat_multiplier(participant),
"initiativeHeat": participant.initiative_heat.as_ref(),
"effectiveCooldownMultiplier": effective_cooldown_multiplier(participant),
"personalityTiming": participant.extra.get("personalityTiming"),
"moodTiming": participant.extra.get("moodTiming"),
"initiativeFrozen": participant
.pending_turn
.as_ref()
.is_some_and(|turn| turn.status == "running"),
"initiativeFreezeReason": participant
.pending_turn
.as_ref()
.and_then(|turn| turn.initiative_freeze_reason.as_ref()),
"personalityTiming": participant.personality_timing.as_ref(),
"moodTiming": participant.mood_timing.as_ref(),
"nextReadyAt": participant.next_ready_at,
"reactionBias": participant.reaction_bias,
"interruptThreshold": participant.interrupt_threshold,
@ -228,7 +259,16 @@ pub(super) fn schedule_participant_json(participant: &HeartbeatParticipant) -> V
"initiative_speed": participant.initiative_speed,
"personality_cooldown_multiplier": personality_cooldown_multiplier(participant),
"mood_cooldown_multiplier": mood_cooldown_multiplier(participant),
"initiative_heat_multiplier": initiative_heat_multiplier(participant),
"effective_cooldown_multiplier": effective_cooldown_multiplier(participant),
"initiative_frozen": participant
.pending_turn
.as_ref()
.is_some_and(|turn| turn.status == "running"),
"initiative_freeze_reason": participant
.pending_turn
.as_ref()
.and_then(|turn| turn.initiative_freeze_reason.as_ref()),
"next_ready_at": participant.next_ready_at,
"reaction_bias": participant.reaction_bias,
"interrupt_threshold": participant.interrupt_threshold,
@ -259,9 +299,12 @@ pub(super) fn pending_turn_json(turn: &HeartbeatPendingTurn) -> Value {
"startedAt": turn.started_at,
"startedSceneClock": turn.started_scene_clock,
"baseRecovery": turn.base_recovery,
"personalityCooldownMultiplier": turn.extra.get("personalityCooldownMultiplier"),
"moodCooldownMultiplier": turn.extra.get("moodCooldownMultiplier"),
"effectiveCooldownMultiplier": turn.extra.get("effectiveCooldownMultiplier"),
"personalityCooldownMultiplier": turn.personality_cooldown_multiplier,
"moodCooldownMultiplier": turn.mood_cooldown_multiplier,
"initiativeHeatMultiplier": turn.initiative_heat_multiplier,
"effectiveCooldownMultiplier": turn.effective_cooldown_multiplier,
"initiativeFrozen": turn.initiative_frozen,
"initiativeFreezeReason": turn.initiative_freeze_reason.as_ref(),
"recovery": turn.recovery,
"cooldownPolicy": turn.cooldown_policy,
"completedAt": turn.completed_at,

View file

@ -5,13 +5,13 @@ use super::HEARTBEAT_ARENA_SCENE;
use super::HEARTBEAT_STATE_SCHEMA_VERSION;
use super::HeartbeatPacingPolicy;
use super::HeartbeatParticipant;
use super::HeartbeatProtocol;
use super::HeartbeatSelectionPolicy;
use super::PARTICIPANT_KIND_AGENT;
use super::PARTICIPANT_KIND_CHARACTER;
use super::write_heartbeat_state_entry;
use anyhow::Result;
use anyhow::anyhow;
use std::collections::BTreeMap;
use std::path::Path;
pub(super) const ROLE_ORDER: &[&str] = &[
@ -40,7 +40,6 @@ pub fn default_heartbeat_state(target_heartbeat_rate: f64) -> EpiphanyHeartbeatS
"initiative_speed_desc".to_string(),
"stable_actor_id_asc".to_string(),
],
extra: BTreeMap::new(),
},
pacing_policy: HeartbeatPacingPolicy {
cooldown_starts_after_turn_completion: true,
@ -48,14 +47,16 @@ pub fn default_heartbeat_state(target_heartbeat_rate: f64) -> EpiphanyHeartbeatS
idle_base_recovery: 2.0,
sleep_heartbeat_rate_multiplier: 0.05,
minimum_effective_rate: 0.001,
extra: BTreeMap::new(),
},
participants: ROLE_ORDER
.iter()
.map(|role_id| default_participant(role_id))
.collect(),
history: Vec::new(),
extra: BTreeMap::new(),
initiative_heat: Default::default(),
protocol: None,
adaptive_pacing: None,
pending_mentions: Vec::new(),
}
}
@ -82,15 +83,12 @@ pub fn ghostlight_scene_heartbeat_state(
.into_iter()
.map(|seed| ghostlight_scene_participant(&scene_id, seed))
.collect();
state.extra.insert(
"protocol".to_string(),
serde_json::json!({
"domain": "ghostlight",
"sceneId": scene_id,
"arena": HEARTBEAT_ARENA_SCENE,
"contract": "Characters and maintenance organs use one initiative timing law; scene participants receive only projected local context.",
}),
);
state.protocol = Some(HeartbeatProtocol {
domain: "ghostlight".to_string(),
scene_id: Some(scene_id),
arena: HEARTBEAT_ARENA_SCENE.to_string(),
contract: "Characters and maintenance organs use one initiative timing law; scene participants receive only projected local context.".to_string(),
});
super::validate_heartbeat_state(&state)?;
Ok(state)
}
@ -126,7 +124,15 @@ pub(super) fn default_participant(role_id: &str) -> HeartbeatParticipant {
last_woke_at: None,
last_finished_at: None,
pending_turn: None,
extra: BTreeMap::new(),
scene_id: None,
groups: Vec::new(),
personality_cooldown_multiplier: 1.0,
mood_cooldown_multiplier: 1.0,
initiative_heat_multiplier: 1.0,
initiative_heat: None,
personality_timing: None,
mood_timing: None,
birth_personality_seed: None,
}
}
@ -135,8 +141,6 @@ fn ghostlight_scene_participant(
seed: GhostlightSceneParticipantSeed,
) -> HeartbeatParticipant {
let role_id = ghostlight_role_id(seed.agent_id.as_str());
let mut extra = BTreeMap::new();
extra.insert("sceneId".to_string(), serde_json::json!(scene_id));
HeartbeatParticipant {
agent_id: seed.agent_id,
role_id,
@ -154,7 +158,15 @@ fn ghostlight_scene_participant(
last_woke_at: None,
last_finished_at: None,
pending_turn: None,
extra,
scene_id: Some(scene_id.to_string()),
groups: Vec::new(),
personality_cooldown_multiplier: 1.0,
mood_cooldown_multiplier: 1.0,
initiative_heat_multiplier: 1.0,
initiative_heat: None,
personality_timing: None,
mood_timing: None,
birth_personality_seed: None,
}
}
@ -254,7 +266,9 @@ fn participant_constraints(role_id: &str) -> Vec<&'static str> {
}
"imagination" => "Synthesizes futures; must not adopt objectives.",
"research" => "Scouts known work; must not turn research into procrastination.",
"modeling" => "Grows source-grounded maps and checkpoints; must not edit implementation code.",
"modeling" => {
"Grows source-grounded maps and checkpoints; must not edit implementation code."
}
"implementation" => {
"Touches source only with accepted guidance and verifier-readable evidence."
}
@ -320,12 +334,12 @@ mod tests {
assert_eq!(participant.role_id, "ghostlight.character.ariadne-prime");
assert_eq!(participant.arena, HEARTBEAT_ARENA_SCENE);
assert_eq!(participant.participant_kind, PARTICIPANT_KIND_CHARACTER);
assert_eq!(participant.scene_id.as_deref(), Some("room"));
assert_eq!(
state
.extra
.get("protocol")
.and_then(|value| value.get("domain"))
.and_then(|value| value.as_str()),
.protocol
.as_ref()
.map(|protocol| protocol.domain.as_str()),
Some("ghostlight")
);
Ok(())

View file

@ -17,7 +17,6 @@ use anyhow::anyhow;
use cultcache_rs::CultCache;
use cultcache_rs::SingleFileMessagePackBackingStore;
use serde_json::Value;
use std::collections::BTreeMap;
use std::path::Path;
pub fn heartbeat_state_cache(store_path: impl AsRef<Path>) -> Result<CultCache> {
@ -59,15 +58,15 @@ pub fn load_heartbeat_cognition_entry(
) -> Result<Option<EpiphanyHeartbeatCognitionEntry>> {
let store_path = store_path.as_ref();
let cache = heartbeat_state_cache(store_path)?;
if let Some(cognition) = cache.get::<EpiphanyHeartbeatCognitionEntry>(HEARTBEAT_COGNITION_KEY)?
if let Some(cognition) =
cache.get::<EpiphanyHeartbeatCognitionEntry>(HEARTBEAT_COGNITION_KEY)?
{
return Ok(Some(cognition));
}
let legacy_cache = legacy_heartbeat_state_cache(store_path)?;
let Some(legacy) =
legacy_cache.get::<LegacyHeartbeatStateWithCognition>(HEARTBEAT_STATE_KEY)?
else {
return Ok(None);
let legacy = match legacy_cache.get::<LegacyHeartbeatStateWithCognition>(HEARTBEAT_STATE_KEY) {
Ok(Some(legacy)) => legacy,
Ok(None) | Err(_) => return Ok(None),
};
legacy_heartbeat_cognition_entry(legacy)
}
@ -126,18 +125,27 @@ fn legacy_heartbeat_cognition_entry(
latest_run_id,
latest_artifact_ref: None,
source,
sleep_cycle: legacy.sleep_cycle,
memory_resonance: legacy.memory_resonance,
incubation: legacy.incubation,
thought_lanes: legacy.thought_lanes,
bridge: legacy.bridge,
candidate_interventions: legacy.candidate_interventions,
appraisals: legacy.appraisals,
reactions: legacy.reactions,
extra: BTreeMap::new(),
sleep_cycle: decode_legacy_document(legacy.sleep_cycle)?,
memory_resonance: decode_legacy_document(legacy.memory_resonance)?,
incubation: decode_legacy_document(legacy.incubation)?,
thought_lanes: decode_legacy_document(legacy.thought_lanes)?,
bridge: decode_legacy_document(legacy.bridge)?,
candidate_interventions: decode_legacy_document(legacy.candidate_interventions)?,
appraisals: decode_legacy_document(legacy.appraisals)?,
reactions: decode_legacy_document(legacy.reactions)?,
}))
}
fn decode_legacy_document<T>(value: Option<Value>) -> Result<Option<T>>
where
T: serde::de::DeserializeOwned,
{
value
.map(serde_json::from_value)
.transpose()
.map_err(Into::into)
}
pub fn validate_heartbeat_state(state: &EpiphanyHeartbeatStateEntry) -> Result<()> {
if state.schema_version != HEARTBEAT_STATE_SCHEMA_VERSION {
return Err(anyhow!(
@ -154,6 +162,29 @@ pub fn validate_heartbeat_state(state: &EpiphanyHeartbeatStateEntry) -> Result<(
"heartbeat target_heartbeat_rate must be non-negative"
));
}
if state.initiative_heat.global_multiplier <= 0.0 {
return Err(anyhow!(
"heartbeat initiative_heat global_multiplier must be positive"
));
}
for multiplier in &state.initiative_heat.multipliers {
if multiplier.id.trim().is_empty() {
return Err(anyhow!("heartbeat initiative heat multiplier has empty id"));
}
if multiplier.multiplier <= 0.0 {
return Err(anyhow!(
"heartbeat initiative heat multiplier {} must be positive",
multiplier.id
));
}
if multiplier.selector.trim().is_empty() && multiplier.scope != "all" {
return Err(anyhow!(
"heartbeat initiative heat multiplier {} selector is empty for scope {}",
multiplier.id,
multiplier.scope
));
}
}
for participant in &state.participants {
if participant.agent_id.trim().is_empty() {
return Err(anyhow!("heartbeat participant has empty agent_id"));
@ -226,14 +257,16 @@ mod tests {
candidate_interventions: None,
appraisals: None,
reactions: None,
extra: BTreeMap::new(),
};
write_heartbeat_cognition_entry(&store_path, &cognition)?;
let loaded_cognition = load_heartbeat_cognition_entry(&store_path)?
.expect("heartbeat cognition should round-trip through CultCache");
assert_eq!(loaded_cognition.schema_version, HEARTBEAT_COGNITION_SCHEMA_VERSION);
assert_eq!(
loaded_cognition.schema_version,
HEARTBEAT_COGNITION_SCHEMA_VERSION
);
assert_eq!(loaded_cognition.latest_run_id.as_deref(), Some("run-1"));
Ok(())
}

View file

@ -1,9 +1,15 @@
mod agent_memory;
mod agent_utterance_state;
mod body_gateway;
mod cultmesh_integration;
mod distillation;
mod eyes_gateway;
mod face_turn;
mod heartbeat_state;
mod memory_graph;
mod mind_gateway;
mod organ_dependencies;
mod promotion;
mod prompt;
mod proposal;
mod retrieval;
mod runtime_spine;
@ -18,7 +24,11 @@ pub use agent_memory::AgentSelfPatch;
pub use agent_memory::EpiphanyAgentMemoryEntry;
pub use agent_memory::EpiphanyDossierProfile;
pub use agent_memory::EpiphanyDossierProfileKind;
pub use agent_memory::GhostlightEvent;
pub use agent_memory::GhostlightMemory;
pub use agent_memory::GhostlightPerceivedStateOverlay;
pub use agent_memory::GhostlightRelationship;
pub use agent_memory::GhostlightScene;
pub use agent_memory::GhostlightTraitVector;
pub use agent_memory::agent_memory_role_ids;
pub use agent_memory::agent_memory_status;
@ -32,12 +42,79 @@ pub use agent_memory::project_agent_memory_to_json_dir;
pub use agent_memory::review_agent_self_patch;
pub use agent_memory::review_agent_self_patch_document;
pub use agent_memory::validate_agent_memory_store;
pub use agent_utterance_state::AGENT_CHARACTER_STATE_VECTOR_SIZE;
pub use agent_utterance_state::AGENT_UTTERANCE_STATE_SCHEMA_VERSION;
pub use agent_utterance_state::AgentUtteranceActivation;
pub use agent_utterance_state::AgentUtteranceCharacterStateAudit;
pub use agent_utterance_state::AgentUtteranceCharacterStateSlot;
pub use agent_utterance_state::AgentUtteranceCharacterStateVector;
pub use agent_utterance_state::AgentUtteranceEmbeddingBasis;
pub use agent_utterance_state::AgentUtteranceIdentity;
pub use agent_utterance_state::AgentUtteranceMood;
pub use agent_utterance_state::AgentUtterancePersonalityVectors;
pub use agent_utterance_state::AgentUtteranceState;
pub use agent_utterance_state::AgentUtteranceValue;
pub use agent_utterance_state::WEKSA_UTTERANCE_HANDOFF_SCHEMA_VERSION;
pub use agent_utterance_state::derive_agent_utterance_state;
pub use body_gateway::BODY_REPO_ACCESS_GRANT_RECEIPT_SCHEMA_VERSION;
pub use body_gateway::BODY_REPO_ACCESS_GRANT_RECEIPT_TYPE;
pub use body_gateway::BODY_REPO_ACCESS_REFUSAL_RECEIPT_SCHEMA_VERSION;
pub use body_gateway::BODY_REPO_ACCESS_REFUSAL_RECEIPT_TYPE;
pub use body_gateway::BODY_REPO_ACCESS_REQUEST_SCHEMA_VERSION;
pub use body_gateway::BODY_REPO_ACCESS_REQUEST_TYPE;
pub use body_gateway::BODY_REPO_ACCESS_REVIEW_SCHEMA_VERSION;
pub use body_gateway::BODY_REPO_ACCESS_REVIEW_TYPE;
pub use body_gateway::BODY_REPO_MUTATION_RECEIPT_SCHEMA_VERSION;
pub use body_gateway::BODY_REPO_MUTATION_RECEIPT_TYPE;
pub use body_gateway::BODY_REPO_SNAPSHOT_RECEIPT_SCHEMA_VERSION;
pub use body_gateway::BODY_REPO_SNAPSHOT_RECEIPT_TYPE;
pub use body_gateway::BodyCultNetContract;
pub use body_gateway::default_body_cultnet_contracts;
pub use cultcache_rs::CacheBackingStore;
pub use cultcache_rs::CultCache;
pub use cultcache_rs::CultCacheEnvelope;
pub use cultcache_rs::DatabaseEntry;
pub use cultcache_rs::PushAllOptions;
pub use cultcache_rs::SingleFileMessagePackBackingStore;
pub use cultmesh_integration::EPIPHANY_CULTMESH_BODY_CONTRACT_SCHEMA_VERSION;
pub use cultmesh_integration::EPIPHANY_CULTMESH_BODY_CONTRACT_TYPE;
pub use cultmesh_integration::EPIPHANY_CULTMESH_EYES_CONTRACT_SCHEMA_VERSION;
pub use cultmesh_integration::EPIPHANY_CULTMESH_EYES_CONTRACT_TYPE;
pub use cultmesh_integration::EPIPHANY_CULTMESH_GLOBAL_ROOM_POLICY_SCHEMA_VERSION;
pub use cultmesh_integration::EPIPHANY_CULTMESH_GLOBAL_ROOM_POLICY_TYPE;
pub use cultmesh_integration::EPIPHANY_CULTMESH_GLOBAL_TIER;
pub use cultmesh_integration::EPIPHANY_CULTMESH_GLOBAL_VERSE_ID;
pub use cultmesh_integration::EPIPHANY_CULTMESH_INTERNAL_TIER;
pub use cultmesh_integration::EPIPHANY_CULTMESH_INTERNAL_VERSE_ID;
pub use cultmesh_integration::EPIPHANY_CULTMESH_LOCAL_AREA_TIER;
pub use cultmesh_integration::EPIPHANY_CULTMESH_LOCAL_AREA_VERSE_ID;
pub use cultmesh_integration::EPIPHANY_CULTMESH_MIND_CONTRACT_SCHEMA_VERSION;
pub use cultmesh_integration::EPIPHANY_CULTMESH_MIND_CONTRACT_TYPE;
pub use cultmesh_integration::EPIPHANY_CULTMESH_STATUS_KEY;
pub use cultmesh_integration::EPIPHANY_CULTMESH_STATUS_SCHEMA_VERSION;
pub use cultmesh_integration::EPIPHANY_CULTMESH_STATUS_TYPE;
pub use cultmesh_integration::EPIPHANY_CULTMESH_VERSE_POLICY_SCHEMA_VERSION;
pub use cultmesh_integration::EPIPHANY_CULTMESH_VERSE_POLICY_TYPE;
pub use cultmesh_integration::EpiphanyCultMeshBodyContractEntry;
pub use cultmesh_integration::EpiphanyCultMeshDocuments;
pub use cultmesh_integration::EpiphanyCultMeshEyesContractEntry;
pub use cultmesh_integration::EpiphanyCultMeshGlobalRoomPolicyEntry;
pub use cultmesh_integration::EpiphanyCultMeshMindContractEntry;
pub use cultmesh_integration::EpiphanyCultMeshStatusEntry;
pub use cultmesh_integration::EpiphanyCultMeshVersePolicyEntry;
pub use cultmesh_integration::epiphany_cultmesh_body_contracts;
pub use cultmesh_integration::epiphany_cultmesh_eyes_contracts;
pub use cultmesh_integration::epiphany_cultmesh_global_room_policies;
pub use cultmesh_integration::epiphany_cultmesh_mind_contracts;
pub use cultmesh_integration::epiphany_cultmesh_verse_policies;
pub use cultmesh_integration::load_epiphany_cultmesh_status;
pub use cultmesh_integration::open_epiphany_cultmesh_node;
pub use cultmesh_integration::write_epiphany_cultmesh_body_contracts;
pub use cultmesh_integration::write_epiphany_cultmesh_eyes_contracts;
pub use cultmesh_integration::write_epiphany_cultmesh_global_room_policies;
pub use cultmesh_integration::write_epiphany_cultmesh_mind_contracts;
pub use cultmesh_integration::write_epiphany_cultmesh_status;
pub use cultmesh_integration::write_epiphany_cultmesh_verse_policies;
pub use distillation::EpiphanyDistillInput;
pub use distillation::EpiphanyDistillProposal;
pub use distillation::distill_observation;
@ -53,6 +130,33 @@ pub use epiphany_state_model::EpiphanyMemoryNode;
pub use epiphany_state_model::EpiphanyMemoryProfile;
pub use epiphany_state_model::EpiphanyMemorySummary;
pub use epiphany_state_model::EpiphanyThreadState;
pub use epiphany_state_model::render_epiphany_state;
pub use eyes_gateway::EYES_EVIDENCE_PACKET_SCHEMA_VERSION;
pub use eyes_gateway::EYES_EVIDENCE_PACKET_TYPE;
pub use eyes_gateway::EYES_EVIDENCE_REFUSAL_RECEIPT_SCHEMA_VERSION;
pub use eyes_gateway::EYES_EVIDENCE_REFUSAL_RECEIPT_TYPE;
pub use eyes_gateway::EYES_EVIDENCE_REQUEST_SCHEMA_VERSION;
pub use eyes_gateway::EYES_EVIDENCE_REQUEST_TYPE;
pub use eyes_gateway::EYES_EVIDENCE_REVIEW_SCHEMA_VERSION;
pub use eyes_gateway::EYES_EVIDENCE_REVIEW_TYPE;
pub use eyes_gateway::EYES_SOURCE_LOOKUP_RECEIPT_SCHEMA_VERSION;
pub use eyes_gateway::EYES_SOURCE_LOOKUP_RECEIPT_TYPE;
pub use eyes_gateway::EyesCultNetContract;
pub use eyes_gateway::default_eyes_cultnet_contracts;
pub use face_turn::FACE_INTERPRETER_PROMPT_SCHEMA_VERSION;
pub use face_turn::FACE_PROJECTOR_PROMPT_SCHEMA_VERSION;
pub use face_turn::FACE_TURN_PROMPT_SCHEMA_VERSION;
pub use face_turn::FaceIdentity;
pub use face_turn::FaceInterpreterInput;
pub use face_turn::FaceProjectorInput;
pub use face_turn::FaceRepoActivity;
pub use face_turn::FaceSocialAffordance;
pub use face_turn::FaceTranscriptMessage;
pub use face_turn::FaceTurnInput;
pub use face_turn::build_face_interpreter_prompt;
pub use face_turn::build_face_projector_prompt;
pub use face_turn::build_face_turn_prompt;
pub use face_turn::face_projected_surface_is_clean;
pub use heartbeat_state::EpiphanyHeartbeatCognitionEntry;
pub use heartbeat_state::EpiphanyHeartbeatStateEntry;
pub use heartbeat_state::GhostlightSceneParticipantSeed;
@ -62,12 +166,27 @@ pub use heartbeat_state::HEARTBEAT_COGNITION_TYPE;
pub use heartbeat_state::HEARTBEAT_STATE_KEY;
pub use heartbeat_state::HEARTBEAT_STATE_SCHEMA_VERSION;
pub use heartbeat_state::HEARTBEAT_STATE_TYPE;
pub use heartbeat_state::HeartbeatAgentReaction;
pub use heartbeat_state::HeartbeatAgentThoughtAppraisal;
pub use heartbeat_state::HeartbeatAppraisalReview;
pub use heartbeat_state::HeartbeatBirthPersonalitySeed;
pub use heartbeat_state::HeartbeatCandidateImplications;
pub use heartbeat_state::HeartbeatCompleteOptions;
pub use heartbeat_state::HeartbeatEmotionalAppraisal;
pub use heartbeat_state::HeartbeatHeatUpdateOptions;
pub use heartbeat_state::HeartbeatHistoryEvent;
pub use heartbeat_state::HeartbeatInitiativeHeatPolicy;
pub use heartbeat_state::HeartbeatInitiativeMultiplier;
pub use heartbeat_state::HeartbeatMoodDimension;
pub use heartbeat_state::HeartbeatMoodTiming;
pub use heartbeat_state::HeartbeatPacingPolicy;
pub use heartbeat_state::HeartbeatParticipant;
pub use heartbeat_state::HeartbeatParticipantLocalContext;
pub use heartbeat_state::HeartbeatPendingMention;
pub use heartbeat_state::HeartbeatPendingTurn;
pub use heartbeat_state::HeartbeatPersonalityProjection;
pub use heartbeat_state::HeartbeatPumpOptions;
pub use heartbeat_state::HeartbeatQueueMentionOptions;
pub use heartbeat_state::HeartbeatSelectionPolicy;
pub use heartbeat_state::HeartbeatTickOptions;
pub use heartbeat_state::VoidRoutineOptions;
@ -81,8 +200,10 @@ pub use heartbeat_state::initialize_heartbeat_store;
pub use heartbeat_state::load_heartbeat_cognition_entry;
pub use heartbeat_state::load_heartbeat_state_entry;
pub use heartbeat_state::pump_heartbeat_store;
pub use heartbeat_state::queue_heartbeat_pending_mention_store;
pub use heartbeat_state::run_void_routine_store;
pub use heartbeat_state::tick_heartbeat_store;
pub use heartbeat_state::update_heartbeat_heat_store;
pub use heartbeat_state::validate_heartbeat_cognition;
pub use heartbeat_state::validate_heartbeat_state;
pub use heartbeat_state::write_heartbeat_cognition_entry;
@ -115,12 +236,40 @@ pub use memory_graph::validate_memory_graph_entry;
pub use memory_graph::validate_memory_graph_snapshot;
pub use memory_graph::write_memory_graph_entry;
pub use memory_graph::write_memory_graph_snapshot;
pub use mind_gateway::MIND_GATEWAY_REVIEW_SCHEMA_VERSION;
pub use mind_gateway::MIND_GATEWAY_REVIEW_TYPE;
pub use mind_gateway::MIND_INTERPRETER_PROMPT_SCHEMA_VERSION;
pub use mind_gateway::MIND_STATE_COMMIT_RECEIPT_SCHEMA_VERSION;
pub use mind_gateway::MIND_STATE_COMMIT_RECEIPT_TYPE;
pub use mind_gateway::MIND_STATE_EFFECT_PROPOSAL_SCHEMA_VERSION;
pub use mind_gateway::MIND_STATE_EFFECT_PROPOSAL_TYPE;
pub use mind_gateway::MIND_STATE_REJECTION_RECEIPT_SCHEMA_VERSION;
pub use mind_gateway::MIND_STATE_REJECTION_RECEIPT_TYPE;
pub use mind_gateway::MIND_THOUGHT_SCHEMA_VERSION;
pub use mind_gateway::MIND_THOUGHT_TYPE;
pub use mind_gateway::MIND_VERSE_ADOPTION_RECEIPT_SCHEMA_VERSION;
pub use mind_gateway::MIND_VERSE_ADOPTION_RECEIPT_TYPE;
pub use mind_gateway::MindCultNetContract;
pub use mind_gateway::MindGatewayDecision;
pub use mind_gateway::MindGatewayReview;
pub use mind_gateway::MindInterpreterPromptInput;
pub use mind_gateway::build_mind_interpreter_prompt;
pub use mind_gateway::default_mind_cultnet_contracts;
pub use mind_gateway::mind_review_allows_state;
pub use mind_gateway::mind_review_reorient_acceptance;
pub use mind_gateway::mind_review_role_acceptance;
pub use organ_dependencies::EPIPHANY_ORGAN_DEPENDENCY_SCHEMA_VERSION;
pub use organ_dependencies::EPIPHANY_STANDING_ORGANS;
pub use organ_dependencies::EpiphanyOrganDependency;
pub use organ_dependencies::default_organ_dependencies_for;
pub use organ_dependencies::default_organ_dependency_matrix;
pub use organ_dependencies::render_organ_dependencies;
pub use organ_dependencies::render_organ_dependency;
pub use promotion::EpiphanyPromotionDecision;
pub use promotion::EpiphanyPromotionInput;
pub use promotion::EpiphanyStateReplacementValidationInput;
pub use promotion::evaluate_promotion;
pub use promotion::validate_state_replacement_patch;
pub use prompt::render_epiphany_state;
pub use proposal::EpiphanyMapProposal;
pub use proposal::EpiphanyMapProposalInput;
pub use proposal::propose_map_update;
@ -201,6 +350,7 @@ pub use state_ledger::close_state_branch;
pub use state_ledger::load_state_ledger;
pub use state_ledger::state_ledger_status;
pub use state_update::EpiphanyStateUpdate;
pub use state_update::EpiphanyStateUpdatedField;
pub use state_update::apply_epiphany_state_update;
pub use state_update::epiphany_state_update_validation_errors;
pub use surfaces::EpiphanyAcceptanceBundle;
@ -213,6 +363,7 @@ pub use surfaces::EpiphanyCoordinatorAction;
pub use surfaces::EpiphanyCoordinatorAutomationAction;
pub use surfaces::EpiphanyCoordinatorCrrcRecommendation;
pub use surfaces::EpiphanyCoordinatorDecision;
pub use surfaces::EpiphanyCoordinatorFindingSignals;
pub use surfaces::EpiphanyCoordinatorInput;
pub use surfaces::EpiphanyCoordinatorRoleId;
pub use surfaces::EpiphanyCoordinatorRoleLane;
@ -220,6 +371,9 @@ pub use surfaces::EpiphanyCoordinatorRoleResultStatus;
pub use surfaces::EpiphanyCoordinatorRoleStatus;
pub use surfaces::EpiphanyCoordinatorSceneAction;
pub use surfaces::EpiphanyCoordinatorSignals;
pub use surfaces::EpiphanyCoordinatorSourceSignals;
pub use surfaces::EpiphanyCoordinatorStatus;
pub use surfaces::EpiphanyCoordinatorStatusInput;
pub use surfaces::EpiphanyCrrcAction;
pub use surfaces::EpiphanyCrrcInput;
pub use surfaces::EpiphanyCrrcRecommendation;
@ -259,6 +413,7 @@ pub use surfaces::EpiphanyReorientDecision;
pub use surfaces::EpiphanyReorientFindingInterpretation;
pub use surfaces::EpiphanyReorientFreshnessStatus;
pub use surfaces::EpiphanyReorientInput;
pub use surfaces::EpiphanyReorientLaunchRequestInput;
pub use surfaces::EpiphanyReorientPressureLevel;
pub use surfaces::EpiphanyReorientReason;
pub use surfaces::EpiphanyReorientStateStatus;
@ -292,14 +447,19 @@ pub use surfaces::EpiphanySceneStateStatus;
pub use surfaces::EpiphanySceneStatusCount;
pub use surfaces::EpiphanySceneSubgoal;
pub use surfaces::EpiphanyTokenUsageSnapshot;
pub use surfaces::EpiphanyViewLens;
pub use surfaces::EpiphanyWorkerLaunchDocument;
pub use surfaces::REORIENT_WORKER_OUTPUT_CONTRACT_ID;
pub use surfaces::ROLE_WORKER_OUTPUT_CONTRACT_ID;
pub use surfaces::build_reorient_acceptance_bundle;
pub use surfaces::build_reorient_job_launch_request;
pub use surfaces::build_role_acceptance_bundle;
pub use surfaces::coordinator_automation_action;
pub use surfaces::crrc_scene_action_to_coordinator_scene_action;
pub use surfaces::default_epiphany_view_lenses;
pub use surfaces::derive_context;
pub use surfaces::derive_coordinator_finding_signals;
pub use surfaces::derive_coordinator_status;
pub use surfaces::derive_freshness;
pub use surfaces::derive_graph_query;
pub use surfaces::derive_jobs;
@ -307,6 +467,10 @@ pub use surfaces::derive_planning_view;
pub use surfaces::derive_pressure_view;
pub use surfaces::derive_role_board;
pub use surfaces::derive_scene;
pub use surfaces::epiphany_view_needs_jobs;
pub use surfaces::epiphany_view_needs_pressure;
pub use surfaces::epiphany_view_needs_reorientation_inputs;
pub use surfaces::epiphany_view_needs_runtime_store;
pub use surfaces::imagination_role_state_patch_policy_errors;
pub use surfaces::interpret_runtime_reorient_worker_result;
pub use surfaces::interpret_runtime_role_worker_result;
@ -315,6 +479,7 @@ pub use surfaces::recommend_coordinator_action;
pub use surfaces::recommend_crrc_action;
pub use surfaces::recommend_reorientation;
pub use surfaces::render_role_board_note;
pub use surfaces::reorient_finding_already_accepted;
pub use surfaces::reorientation_role_status;
pub use surfaces::review_role_self_patch_document;
pub use surfaces::role_board_job_status_to_role_status;

View file

@ -251,7 +251,6 @@ mod tests {
relationships: Vec::new(),
events: Vec::new(),
scenes: Vec::new(),
extra: Default::default(),
};
let snapshot = memory_graph_from_agent_memories("agent-profile", &[entry]);

View file

@ -1,4 +1,26 @@
use crate::heartbeat_state::EpiphanyHeartbeatCognitionEntry;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatBridgeDecision;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatBridgeSynthesis;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatBridgeTension;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatCandidateIntervention;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatCandidateInterventions;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatCognitionBridge;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatIncubation;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatIncubationTheme;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatMemoryResonance;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatMemoryResonancePair;
#[cfg(test)]
use crate::heartbeat_state::HeartbeatSourceCoverage;
use crate::memory_graph::EpiphanyMemoryDomain;
use crate::memory_graph::EpiphanyMemoryGraphSnapshot;
use crate::memory_graph::EpiphanyMemoryLifecycle;
@ -8,7 +30,6 @@ use crate::memory_graph::EpiphanyMemoryProfile;
use crate::memory_graph::EpiphanyMemorySummary;
use crate::memory_graph::memory_graph_domain_id;
use crate::memory_graph::memory_graph_node_id;
use serde_json::Value;
pub fn memory_graph_from_heartbeat_cognition(
graph_id: impl Into<String>,
@ -49,26 +70,18 @@ fn import_resonance(
let Some(resonance) = cognition.memory_resonance.as_ref() else {
return;
};
let Some(pairs) = resonance.get("pairs").and_then(Value::as_array) else {
return;
};
let domain_id = push_domain(
domains,
EpiphanyMemoryProfile::ShortTerm,
"heartbeat resonance",
);
let before = nodes.len();
for pair in pairs.iter().take(8) {
let left = string_at(pair, "leftSummary");
let right = string_at(pair, "rightSummary");
let left_role = string_at(pair, "leftRole");
let right_role = string_at(pair, "rightRole");
let id_seed = format!(
"{}:{}:{}",
left_role,
right_role,
string_at(pair, "leftMemoryId")
);
for pair in resonance.pairs.iter().take(8) {
let left = pair.left_summary.clone();
let right = pair.right_summary.clone();
let left_role = pair.left_role.clone();
let right_role = pair.right_role.clone();
let id_seed = format!("{}:{}:{}", left_role, right_role, pair.left_memory_id);
nodes.push(EpiphanyMemoryNode {
id: memory_graph_node_id(&domain_id, "resonance", id_seed, None),
domain_id: domain_id.clone(),
@ -84,7 +97,7 @@ fn import_resonance(
.to_string(),
source_hashes: vec!["anchor:missing".to_string()],
lifecycle: EpiphanyMemoryLifecycle::Active,
salience: score_to_u32(number_at(pair, "strength")),
salience: score_to_u32(pair.strength),
confidence: 55,
..Default::default()
});
@ -109,44 +122,41 @@ fn import_incubation(
let Some(incubation) = cognition.incubation.as_ref() else {
return;
};
let Some(themes) = incubation.get("themes").and_then(Value::as_array) else {
return;
};
let domain_id = push_domain(
domains,
EpiphanyMemoryProfile::Incubation,
"heartbeat incubation",
);
let before = nodes.len();
for theme in themes.iter().take(12) {
let theme_id = string_at(theme, "themeId");
let status = string_at(theme, "status");
for theme in incubation.themes.iter().take(12) {
let theme_id = theme.theme_id.clone();
let status = theme.status.clone();
nodes.push(EpiphanyMemoryNode {
id: memory_graph_node_id(&domain_id, "incubation", &theme_id, None),
domain_id: domain_id.clone(),
profile: EpiphanyMemoryProfile::Incubation,
kind: EpiphanyMemoryNodeKind::IncubationThread,
title: theme_id.clone(),
claim: string_at(theme, "summary"),
question: string_at_any(
theme,
&["latentQuestion", "question"],
"What is this incubation theme trying to become?",
),
tension: string_at_any(
theme,
&["holdingCloseBecause", "tension"],
"Heartbeat marked this theme but did not preserve a tension.",
),
action_implication: string_at_any(
theme,
&["whyItPulls", "actionImplication"],
"Keep in incubation until sleep or review accounts for it.",
),
claim: theme.summary.clone(),
question: if theme.latent_question.is_empty() {
"What is this incubation theme trying to become?".to_string()
} else {
theme.latent_question.clone()
},
tension: if theme.holding_close_because.is_empty() {
"Heartbeat marked this theme but did not preserve a tension.".to_string()
} else {
theme.holding_close_because.clone()
},
action_implication: if theme.why_it_pulls.is_empty() {
"Keep in incubation until sleep or review accounts for it.".to_string()
} else {
theme.why_it_pulls.clone()
},
source_hashes: vec!["anchor:missing".to_string()],
lifecycle: incubation_lifecycle(&status),
salience: score_to_u32(number_at(theme, "priorityScore")),
confidence: score_to_u32(number_at(theme, "maturation")),
salience: score_to_u32(theme.priority_score),
confidence: score_to_u32(theme.maturation),
..Default::default()
});
}
@ -170,24 +180,21 @@ fn import_candidate_interventions(
let Some(candidates) = cognition.candidate_interventions.as_ref() else {
return;
};
let Some(items) = candidates.get("items").and_then(Value::as_array) else {
return;
};
let domain_id = push_domain(
domains,
EpiphanyMemoryProfile::CandidateIntervention,
"heartbeat candidate interventions",
);
let before = nodes.len();
for item in items.iter().take(8) {
let intervention_id = string_at(item, "interventionId");
for item in candidates.items.iter().take(8) {
let intervention_id = item.intervention_id.clone();
nodes.push(EpiphanyMemoryNode {
id: memory_graph_node_id(&domain_id, "candidate", &intervention_id, None),
domain_id: domain_id.clone(),
profile: EpiphanyMemoryProfile::CandidateIntervention,
kind: EpiphanyMemoryNodeKind::CandidateIntervention,
title: string_at(item, "summary"),
claim: string_at(item, "draft"),
title: item.summary.clone(),
claim: item.draft.clone(),
question: "Should this candidate intervention be spoken, deferred, or retired?"
.to_string(),
tension: "Candidate speech is not authority and still requires review.".to_string(),
@ -195,7 +202,7 @@ fn import_candidate_interventions(
.to_string(),
source_hashes: vec!["anchor:missing".to_string()],
lifecycle: EpiphanyMemoryLifecycle::Queued,
salience: score_to_u32(number_at(item, "noveltyToRoom")),
salience: score_to_u32(item.novelty_to_room),
confidence: 55,
..Default::default()
});
@ -220,10 +227,7 @@ fn import_bridge_pressure(
let Some(bridge) = cognition.bridge.as_ref() else {
return;
};
let decision = bridge
.pointer("/decision/speakDecision")
.and_then(Value::as_str)
.unwrap_or("silence");
let decision = bridge.decision.speak_decision.as_str();
if decision == "silence" {
return;
}
@ -232,10 +236,11 @@ fn import_bridge_pressure(
EpiphanyMemoryProfile::AgencyPressure,
"heartbeat bridge pressure",
);
let reason = bridge
.pointer("/decision/reason")
.and_then(Value::as_str)
.unwrap_or("Bridge pressure has no explicit reason.");
let reason = if bridge.decision.reason.is_empty() {
"Bridge pressure has no explicit reason."
} else {
bridge.decision.reason.as_str()
};
let node = EpiphanyMemoryNode {
id: memory_graph_node_id(&domain_id, "bridge-pressure", decision, None),
domain_id: domain_id.clone(),
@ -327,31 +332,6 @@ fn push_summary(
});
}
fn string_at(value: &Value, key: &str) -> String {
value
.get(key)
.and_then(Value::as_str)
.unwrap_or_default()
.to_string()
}
fn string_at_any(value: &Value, keys: &[&str], fallback: &str) -> String {
keys.iter()
.find_map(|key| {
let value = value.get(key).and_then(Value::as_str)?;
if value.trim().is_empty() {
None
} else {
Some(value.to_string())
}
})
.unwrap_or_else(|| fallback.to_string())
}
fn number_at(value: &Value, key: &str) -> f64 {
value.get(key).and_then(Value::as_f64).unwrap_or_default()
}
fn score_to_u32(value: f64) -> u32 {
if value.is_finite() {
(value.clamp(0.0, 1.0) * 100.0).round() as u32
@ -383,50 +363,90 @@ mod tests {
latest_artifact_ref: None,
source: Some("unit-test".to_string()),
sleep_cycle: None,
memory_resonance: Some(serde_json::json!({
"pairs": [{
"leftRole": "body",
"rightRole": "soul",
"leftMemoryId": "mem-a",
"leftSummary": "Architecture needs anchors.",
"rightSummary": "Verification needs receipts.",
"strength": 0.8
}]
})),
incubation: Some(serde_json::json!({
"themes": [{
"themeId": "theme-body-soul",
"summary": "Body and Soul keep touching evidence boundaries.",
"latentQuestion": "Should this become doctrine?",
"holdingCloseBecause": "It is live but not settled.",
"whyItPulls": "It affects future review gates.",
"status": "deepening",
"priorityScore": 0.72,
"maturation": 0.6
}, {
"themeId": "theme-live-store-shape",
"summary": "Live heartbeat stores may omit optional explanatory fields.",
"status": "deepening"
}]
})),
memory_resonance: Some(HeartbeatMemoryResonance {
schema_version: "epiphany.memory_resonance.v0".to_string(),
updated_at: "2026-05-18T00:00:00Z".to_string(),
source: "unit-test".to_string(),
record_count: 2,
pairs: vec![HeartbeatMemoryResonancePair {
left_role: "body".to_string(),
left_memory_id: "mem-a".to_string(),
left_memory_kind: "note".to_string(),
left_summary: "Architecture needs anchors.".to_string(),
right_role: "soul".to_string(),
right_memory_id: "mem-b".to_string(),
right_memory_kind: "note".to_string(),
right_summary: "Verification needs receipts.".to_string(),
strength: 0.8,
shared_tokens: vec!["evidence".to_string()],
source_roles: vec!["body".to_string(), "soul".to_string()],
source_kinds: vec!["note".to_string()],
evidence_refs: vec!["mem-a".to_string(), "mem-b".to_string()],
}],
}),
incubation: Some(HeartbeatIncubation {
schema_version: "epiphany.incubation.v0".to_string(),
updated_at: "2026-05-18T00:00:00Z".to_string(),
source_coverage: HeartbeatSourceCoverage::default(),
last_incubation_summary: "Body/Soul evidence seam is live.".to_string(),
themes: vec![
HeartbeatIncubationTheme {
theme_id: "theme-body-soul".to_string(),
summary: "Body and Soul keep touching evidence boundaries.".to_string(),
latent_question: "Should this become doctrine?".to_string(),
holding_close_because: "It is live but not settled.".to_string(),
why_it_pulls: "It affects future review gates.".to_string(),
status: "deepening".to_string(),
priority_score: 0.72,
maturation: 0.6,
..Default::default()
},
HeartbeatIncubationTheme {
theme_id: "theme-live-store-shape".to_string(),
summary: "Live heartbeat stores may omit optional explanatory fields."
.to_string(),
status: "deepening".to_string(),
..Default::default()
},
],
}),
thought_lanes: None,
bridge: Some(serde_json::json!({
"decision": {
"speakDecision": "hold",
"reason": "The thought is live but not ready for speech."
}
})),
candidate_interventions: Some(serde_json::json!({
"items": [{
"interventionId": "candidate-theme-body-soul",
"summary": "Possible note",
"draft": "Evidence boundaries are lighting up.",
"noveltyToRoom": 0.7
}]
})),
bridge: Some(HeartbeatCognitionBridge {
schema_version: "epiphany.cognition_bridge.v0".to_string(),
updated_at: "2026-05-18T00:00:00Z".to_string(),
decision: HeartbeatBridgeDecision {
speak_decision: "hold".to_string(),
reason: "The thought is live but not ready for speech.".to_string(),
..Default::default()
},
recent_syntheses: vec![HeartbeatBridgeSynthesis {
summary: "The thought is live but not ready for speech.".to_string(),
..Default::default()
}],
unresolved_tensions: vec![HeartbeatBridgeTension {
topic: "thought authority boundary".to_string(),
summary: "Bridge pressure is not authority.".to_string(),
opened_at: "2026-05-18T00:00:00Z".to_string(),
}],
..Default::default()
}),
candidate_interventions: Some(HeartbeatCandidateInterventions {
schema_version: "epiphany.candidate_interventions.v0".to_string(),
updated_at: "2026-05-18T00:00:00Z".to_string(),
items: vec![HeartbeatCandidateIntervention {
intervention_id: "candidate-theme-body-soul".to_string(),
summary: "Possible note".to_string(),
draft: "Evidence boundaries are lighting up.".to_string(),
decision: "draft".to_string(),
requires_face: true,
requires_review: true,
novelty_to_room: 0.7,
saturation_score: 0.1,
created_at: "2026-05-18T00:00:00Z".to_string(),
}],
}),
appraisals: None,
reactions: None,
extra: Default::default(),
};
let snapshot = memory_graph_from_heartbeat_cognition("heartbeat-profile", &cognition);

View file

@ -0,0 +1,511 @@
use crate::EpiphanyReorientAcceptanceFinding;
use crate::EpiphanyRoleFindingInterpretation;
use crate::EpiphanyRoleResultRoleId;
use crate::EpiphanyRoleStatePatchDocument;
use serde::Deserialize;
use serde::Serialize;
pub const MIND_GATEWAY_REVIEW_SCHEMA_VERSION: &str = "epiphany.mind_gateway_review.v0";
pub const MIND_INTERPRETER_PROMPT_SCHEMA_VERSION: &str = "epiphany.mind_interpreter_prompt.v0";
pub const MIND_THOUGHT_TYPE: &str = "epiphany.mind.thought";
pub const MIND_STATE_EFFECT_PROPOSAL_TYPE: &str = "epiphany.mind.state_effect_proposal";
pub const MIND_GATEWAY_REVIEW_TYPE: &str = "epiphany.mind.gateway_review";
pub const MIND_STATE_COMMIT_RECEIPT_TYPE: &str = "epiphany.mind.state_commit_receipt";
pub const MIND_STATE_REJECTION_RECEIPT_TYPE: &str = "epiphany.mind.state_rejection_receipt";
pub const MIND_VERSE_ADOPTION_RECEIPT_TYPE: &str = "epiphany.mind.verse_adoption_receipt";
pub const MIND_THOUGHT_SCHEMA_VERSION: &str = "epiphany.mind.thought.v0";
pub const MIND_STATE_EFFECT_PROPOSAL_SCHEMA_VERSION: &str =
"epiphany.mind.state_effect_proposal.v0";
pub const MIND_STATE_COMMIT_RECEIPT_SCHEMA_VERSION: &str = "epiphany.mind.state_commit_receipt.v0";
pub const MIND_STATE_REJECTION_RECEIPT_SCHEMA_VERSION: &str =
"epiphany.mind.state_rejection_receipt.v0";
pub const MIND_VERSE_ADOPTION_RECEIPT_SCHEMA_VERSION: &str =
"epiphany.mind.verse_adoption_receipt.v0";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum MindGatewayDecision {
Accept,
Refuse,
Hold,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MindGatewayReview {
pub schema_version: String,
pub gateway_id: String,
pub source_kind: String,
pub source_role_id: String,
pub decision: MindGatewayDecision,
pub allowed_effects: Vec<String>,
pub refused_effects: Vec<String>,
pub reasons: Vec<String>,
pub contract: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MindInterpreterPromptInput {
pub source_kind: String,
pub source_role_id: String,
pub worker_summary: String,
pub proposed_effects: Vec<String>,
pub current_state_context: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MindCultNetContract {
pub contract_id: String,
pub verse_id: String,
pub document_type: String,
pub payload_schema_version: String,
pub authority: String,
pub operations: Vec<String>,
pub intent_document_types: Vec<String>,
pub receipt_document_types: Vec<String>,
pub notes: Vec<String>,
}
pub fn default_mind_cultnet_contracts() -> Vec<MindCultNetContract> {
vec![
MindCultNetContract {
contract_id: "epiphany.mind.thought.submit".to_string(),
verse_id: "epiphany-internal".to_string(),
document_type: MIND_THOUGHT_TYPE.to_string(),
payload_schema_version: MIND_THOUGHT_SCHEMA_VERSION.to_string(),
authority: "subAgentProposal".to_string(),
operations: vec![
"intentSubmit".to_string(),
"receiptWatch".to_string(),
"snapshot".to_string(),
],
intent_document_types: vec![MIND_THOUGHT_TYPE.to_string()],
receipt_document_types: vec![
MIND_GATEWAY_REVIEW_TYPE.to_string(),
MIND_STATE_REJECTION_RECEIPT_TYPE.to_string(),
],
notes: vec![
"Sub-agent output enters as thought, not state authority.".to_string(),
"The internal Verse may carry private thought; local-area and global Verses must not.".to_string(),
],
},
MindCultNetContract {
contract_id: "epiphany.mind.state_effect.review".to_string(),
verse_id: "epiphany-internal".to_string(),
document_type: MIND_STATE_EFFECT_PROPOSAL_TYPE.to_string(),
payload_schema_version: MIND_STATE_EFFECT_PROPOSAL_SCHEMA_VERSION.to_string(),
authority: "mind".to_string(),
operations: vec![
"intentSubmit".to_string(),
"receiptWatch".to_string(),
"snapshot".to_string(),
],
intent_document_types: vec![MIND_STATE_EFFECT_PROPOSAL_TYPE.to_string()],
receipt_document_types: vec![
MIND_GATEWAY_REVIEW_TYPE.to_string(),
MIND_STATE_COMMIT_RECEIPT_TYPE.to_string(),
MIND_STATE_REJECTION_RECEIPT_TYPE.to_string(),
],
notes: vec![
"Mind is the persistent state guardian: every proposed durable state effect is reviewed here.".to_string(),
"Role acceptance, reorientation acceptance, Face Interpreter effects, selfPatch, evidence, scratch, checkpoints, graph changes, and objective changes share this gate.".to_string(),
],
},
MindCultNetContract {
contract_id: "epiphany.mind.review.snapshot".to_string(),
verse_id: "epiphany-internal".to_string(),
document_type: MIND_GATEWAY_REVIEW_TYPE.to_string(),
payload_schema_version: MIND_GATEWAY_REVIEW_SCHEMA_VERSION.to_string(),
authority: "readOnly".to_string(),
operations: vec!["snapshot".to_string(), "receiptWatch".to_string()],
intent_document_types: Vec::new(),
receipt_document_types: Vec::new(),
notes: vec![
"Mind reviews are receipts. They explain accepted, refused, or held state effects.".to_string(),
],
},
MindCultNetContract {
contract_id: "epiphany.mind.public_adoption.review".to_string(),
verse_id: "epiphany-global".to_string(),
document_type: MIND_VERSE_ADOPTION_RECEIPT_TYPE.to_string(),
payload_schema_version: MIND_VERSE_ADOPTION_RECEIPT_SCHEMA_VERSION.to_string(),
authority: "mind".to_string(),
operations: vec!["receiptWatch".to_string(), "snapshot".to_string()],
intent_document_types: vec!["epiphany.public_dream.v0".to_string()],
receipt_document_types: vec![MIND_VERSE_ADOPTION_RECEIPT_TYPE.to_string()],
notes: vec![
"Global Verse material is public thought weather until Mind emits a local adoption receipt.".to_string(),
"Public adoption receipts may reference local state effects but do not grant the global Verse private state authority.".to_string(),
],
},
]
}
pub fn build_mind_interpreter_prompt(input: &MindInterpreterPromptInput) -> String {
format!(
r#"<!-- prompt:{schema} -->
You are Mind, the gateway between thought and state.
The state is the Mind. Sub-agents may think, inspect, narrate, propose, verify, or remember, but no sub-agent output becomes durable state merely because it contains the right field.
Your job:
- Read the worker thought as input, not authority.
- Decide which proposed effects may enter durable state.
- Refuse project truth disguised as memory, authority grabs, ungrounded graph changes, raw transcript dumps, and convenient state cargo outside the role's authority.
- Return only a concise Mind route: ACCEPT, REFUSE, or HOLD, with allowed effects, refused effects, and reasons.
Source kind: {source_kind}
Source role: {source_role}
Worker summary:
{summary}
Proposed effects:
{effects}
Current state context:
{state_context}
"#,
schema = MIND_INTERPRETER_PROMPT_SCHEMA_VERSION,
source_kind = input.source_kind,
source_role = input.source_role_id,
summary = input.worker_summary,
effects = render_list(&input.proposed_effects),
state_context = input.current_state_context,
)
}
pub fn mind_review_role_acceptance(
binding_id: &str,
role_id: EpiphanyRoleResultRoleId,
finding: &EpiphanyRoleFindingInterpretation,
proposed_patch: &EpiphanyRoleStatePatchDocument,
) -> MindGatewayReview {
let mut reasons = Vec::new();
let mut refused_effects = Vec::new();
let mut allowed_effects = state_patch_effects(proposed_patch);
allowed_effects.extend([
"acceptanceReceipt".to_string(),
"observation".to_string(),
"evidence".to_string(),
]);
if finding.self_patch.is_some() {
allowed_effects.push("selfPatchReview".to_string());
}
if finding
.runtime_result_id
.as_deref()
.unwrap_or_default()
.is_empty()
{
reasons.push("Mind refuses state entry without runtime result identity.".to_string());
refused_effects.push("acceptanceReceipt".to_string());
}
if finding
.runtime_job_id
.as_deref()
.unwrap_or_default()
.is_empty()
{
reasons.push("Mind refuses state entry without runtime job identity.".to_string());
refused_effects.push("acceptanceReceipt".to_string());
}
if let Some(error) = &finding.job_error {
reasons.push(format!("Mind refuses errored worker job: {error}"));
refused_effects.push("workerFinding".to_string());
}
if let Some(error) = &finding.item_error {
reasons.push(format!("Mind refuses unreviewable worker item: {error}"));
refused_effects.push("workerFinding".to_string());
}
if matches!(
role_id,
EpiphanyRoleResultRoleId::Implementation | EpiphanyRoleResultRoleId::Reorientation
) {
reasons.push(format!(
"Mind refuses {role_id:?} through role acceptance; this lane has no roleAccept state authority."
));
refused_effects.push("statePatch".to_string());
}
MindGatewayReview {
schema_version: MIND_GATEWAY_REVIEW_SCHEMA_VERSION.to_string(),
gateway_id: format!("mind-role-{binding_id}"),
source_kind: "roleWorkerResult".to_string(),
source_role_id: role_label(role_id).to_string(),
decision: if reasons.is_empty() {
MindGatewayDecision::Accept
} else {
MindGatewayDecision::Refuse
},
allowed_effects: if reasons.is_empty() {
unique(allowed_effects)
} else {
Vec::new()
},
refused_effects: unique(refused_effects),
reasons,
contract: "Mind is the only gateway from sub-agent output into durable state; workers produce thought, Mind permits state effects.".to_string(),
}
}
pub fn mind_review_reorient_acceptance(
binding_id: &str,
finding: &EpiphanyReorientAcceptanceFinding,
update_scratch: bool,
update_investigation_checkpoint: bool,
) -> MindGatewayReview {
let mut reasons = Vec::new();
let mut refused_effects = Vec::new();
let mut allowed_effects = vec![
"acceptanceReceipt".to_string(),
"observation".to_string(),
"evidence".to_string(),
];
if update_scratch {
allowed_effects.push("scratch".to_string());
}
if update_investigation_checkpoint {
allowed_effects.push("investigationCheckpoint".to_string());
}
if finding
.runtime_result_id
.as_deref()
.unwrap_or_default()
.is_empty()
{
reasons.push(
"Mind refuses reorientation state entry without runtime result identity.".to_string(),
);
refused_effects.push("acceptanceReceipt".to_string());
}
if finding
.runtime_job_id
.as_deref()
.unwrap_or_default()
.is_empty()
{
reasons.push(
"Mind refuses reorientation state entry without runtime job identity.".to_string(),
);
refused_effects.push("acceptanceReceipt".to_string());
}
MindGatewayReview {
schema_version: MIND_GATEWAY_REVIEW_SCHEMA_VERSION.to_string(),
gateway_id: format!("mind-reorient-{binding_id}"),
source_kind: "reorientWorkerResult".to_string(),
source_role_id: "reorientation".to_string(),
decision: if reasons.is_empty() {
MindGatewayDecision::Accept
} else {
MindGatewayDecision::Refuse
},
allowed_effects: if reasons.is_empty() {
unique(allowed_effects)
} else {
Vec::new()
},
refused_effects: unique(refused_effects),
reasons,
contract: "Mind is the only gateway from sub-agent output into durable state; reorientation findings can update continuity state only after Mind review.".to_string(),
}
}
pub fn mind_review_allows_state(review: &MindGatewayReview) -> Result<(), String> {
match review.decision {
MindGatewayDecision::Accept => Ok(()),
MindGatewayDecision::Refuse | MindGatewayDecision::Hold => Err(format!(
"Mind refused state entry: {}",
if review.reasons.is_empty() {
"no reason supplied".to_string()
} else {
review.reasons.join("; ")
}
)),
}
}
fn state_patch_effects(patch: &EpiphanyRoleStatePatchDocument) -> Vec<String> {
let mut effects = Vec::new();
if patch.objective.is_some() {
effects.push("objective".to_string());
}
if patch.active_subgoal_id.is_some() {
effects.push("activeSubgoalId".to_string());
}
if patch.subgoals.is_some() {
effects.push("subgoals".to_string());
}
if patch.invariants.is_some() {
effects.push("invariants".to_string());
}
if patch.graphs.is_some() {
effects.push("graphs".to_string());
}
if patch.graph_frontier.is_some() {
effects.push("graphFrontier".to_string());
}
if patch.graph_checkpoint.is_some() {
effects.push("graphCheckpoint".to_string());
}
if patch.scratch.is_some() {
effects.push("scratch".to_string());
}
if patch.investigation_checkpoint.is_some() {
effects.push("investigationCheckpoint".to_string());
}
if patch.planning.is_some() {
effects.push("planning".to_string());
}
if !patch.observations.is_empty() {
effects.push("observations".to_string());
}
if !patch.evidence.is_empty() {
effects.push("evidence".to_string());
}
effects
}
fn role_label(role_id: EpiphanyRoleResultRoleId) -> &'static str {
match role_id {
EpiphanyRoleResultRoleId::Implementation => "implementation",
EpiphanyRoleResultRoleId::Imagination => "imagination",
EpiphanyRoleResultRoleId::Modeling => "modeling",
EpiphanyRoleResultRoleId::Verification => "verification",
EpiphanyRoleResultRoleId::Reorientation => "reorientation",
}
}
fn render_list(items: &[String]) -> String {
if items.is_empty() {
return "- none".to_string();
}
items
.iter()
.map(|item| format!("- {item}"))
.collect::<Vec<_>>()
.join("\n")
}
fn unique(items: Vec<String>) -> Vec<String> {
let mut out = Vec::new();
for item in items {
if !out.contains(&item) {
out.push(item);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn finding() -> EpiphanyRoleFindingInterpretation {
EpiphanyRoleFindingInterpretation {
verdict: Some("checkpoint-ready".to_string()),
summary: Some("Body mapped the seam.".to_string()),
next_safe_move: None,
checkpoint_summary: None,
scratch_summary: None,
files_inspected: Vec::new(),
frontier_node_ids: Vec::new(),
evidence_ids: Vec::new(),
artifact_refs: Vec::new(),
runtime_result_id: Some("result-1".to_string()),
runtime_job_id: Some("job-1".to_string()),
open_questions: Vec::new(),
evidence_gaps: Vec::new(),
risks: Vec::new(),
state_patch: None,
self_patch: None,
self_persistence: None,
job_error: None,
item_error: None,
}
}
#[test]
fn mind_accepts_reviewable_modeling_state_effects() {
let patch = EpiphanyRoleStatePatchDocument {
scratch: Some(epiphany_state_model::EpiphanyScratchPad {
summary: Some("Mapped seam.".to_string()),
..Default::default()
}),
..Default::default()
};
let review = mind_review_role_acceptance(
"binding-modeling",
EpiphanyRoleResultRoleId::Modeling,
&finding(),
&patch,
);
assert_eq!(review.decision, MindGatewayDecision::Accept);
assert!(review.allowed_effects.contains(&"scratch".to_string()));
assert!(
review
.allowed_effects
.contains(&"acceptanceReceipt".to_string())
);
assert!(mind_review_allows_state(&review).is_ok());
}
#[test]
fn mind_refuses_worker_output_without_runtime_identity() {
let mut finding = finding();
finding.runtime_result_id = None;
let review = mind_review_role_acceptance(
"binding-modeling",
EpiphanyRoleResultRoleId::Modeling,
&finding,
&EpiphanyRoleStatePatchDocument::default(),
);
assert_eq!(review.decision, MindGatewayDecision::Refuse);
assert!(mind_review_allows_state(&review).is_err());
}
#[test]
fn mind_interpreter_prompt_names_state_gateway() {
let prompt = build_mind_interpreter_prompt(&MindInterpreterPromptInput {
source_kind: "roleWorkerResult".to_string(),
source_role_id: "modeling".to_string(),
worker_summary: "Body proposes a graph patch.".to_string(),
proposed_effects: vec!["graphs".to_string()],
current_state_context: "revision 7".to_string(),
});
assert!(prompt.contains("The state is the Mind"));
assert!(prompt.contains("Sub-agents may think"));
assert!(prompt.contains("no sub-agent output becomes durable state"));
}
#[test]
fn mind_cultnet_contracts_make_internal_verse_the_private_state_gate() {
let contracts = default_mind_cultnet_contracts();
let state_review = contracts
.iter()
.find(|contract| contract.contract_id == "epiphany.mind.state_effect.review")
.expect("state effect review contract");
let public_adoption = contracts
.iter()
.find(|contract| contract.contract_id == "epiphany.mind.public_adoption.review")
.expect("public adoption contract");
assert_eq!(state_review.verse_id, "epiphany-internal");
assert_eq!(state_review.authority, "mind");
assert!(
state_review
.receipt_document_types
.contains(&MIND_STATE_COMMIT_RECEIPT_TYPE.to_string())
);
assert_eq!(public_adoption.verse_id, "epiphany-global");
assert!(
public_adoption
.notes
.iter()
.any(|note| note.contains("do not grant the global Verse private state authority"))
);
}
}

View file

@ -0,0 +1,96 @@
use serde::Deserialize;
use serde::Serialize;
pub const EPIPHANY_ORGAN_DEPENDENCY_SCHEMA_VERSION: &str = "epiphany.organ_dependency.v0";
pub const EPIPHANY_STANDING_ORGANS: [&str; 8] = [
"self",
"face",
"imagination",
"eyes",
"body",
"hands",
"soul",
"life",
];
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyOrganDependency {
pub schema_version: String,
pub organ_id: String,
pub depends_on: Vec<String>,
pub contract: String,
}
pub fn default_organ_dependencies_for(organ_id: &str) -> EpiphanyOrganDependency {
let normalized = organ_id.trim().to_ascii_lowercase();
EpiphanyOrganDependency {
schema_version: EPIPHANY_ORGAN_DEPENDENCY_SCHEMA_VERSION.to_string(),
organ_id: normalized.clone(),
depends_on: EPIPHANY_STANDING_ORGANS
.iter()
.filter(|candidate| **candidate != normalized)
.map(|candidate| (*candidate).to_string())
.collect(),
contract: "Every sub-agent depends on the other organs: Self routes, Face speaks, Imagination projects futures/scenes, Eyes seeks evidence, Body gates substrate access, Hands acts, Soul verifies, and Life preserves continuity.".to_string(),
}
}
pub fn default_organ_dependency_matrix() -> Vec<EpiphanyOrganDependency> {
EPIPHANY_STANDING_ORGANS
.iter()
.map(|organ| default_organ_dependencies_for(organ))
.collect()
}
pub fn render_organ_dependency(dependency: &EpiphanyOrganDependency) -> String {
let depends_on = if dependency.depends_on.is_empty() {
"- none".to_string()
} else {
dependency
.depends_on
.iter()
.map(|organ| format!("- {organ}"))
.collect::<Vec<_>>()
.join("\n")
};
format!(
"Organ: {}\nDepends on:\n{}\nContract: {}",
dependency.organ_id, depends_on, dependency.contract
)
}
pub fn render_organ_dependencies(dependencies: &[EpiphanyOrganDependency]) -> String {
if dependencies.is_empty() {
return "- no organ dependency records supplied".to_string();
}
dependencies
.iter()
.map(render_organ_dependency)
.collect::<Vec<_>>()
.join("\n\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_standing_organ_depends_on_every_other_standing_organ() {
let matrix = default_organ_dependency_matrix();
assert_eq!(matrix.len(), EPIPHANY_STANDING_ORGANS.len());
for dependency in matrix {
assert_eq!(
dependency.depends_on.len(),
EPIPHANY_STANDING_ORGANS.len() - 1
);
assert!(!dependency.depends_on.contains(&dependency.organ_id));
for organ in EPIPHANY_STANDING_ORGANS {
if organ != dependency.organ_id {
assert!(dependency.depends_on.contains(&organ.to_string()));
}
}
}
}
}

View file

@ -1143,7 +1143,7 @@ mod tests {
summary: "The prompt renderer injects Epiphany state".to_string(),
source_kind: "verification".to_string(),
status: status.to_string(),
code_refs: vec![code_ref("epiphany-core/src/prompt.rs")],
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
evidence_ids: vec!["ev-verified".to_string()],
}],
recent_evidence: vec![EpiphanyEvidenceRecord {
@ -1151,7 +1151,7 @@ mod tests {
kind: "verification".to_string(),
status: "ok".to_string(),
summary: "Verified prompt renderer behavior".to_string(),
code_refs: vec![code_ref("epiphany-core/src/prompt.rs")],
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
}],
churn: Some(EpiphanyChurnState {
understanding_status: "grounded".to_string(),
@ -1197,11 +1197,11 @@ mod tests {
title: "Prompt renderer".to_string(),
purpose: "Render Epiphany state into developer context".to_string(),
status: Some("grounded".to_string()),
code_refs: vec![code_ref("epiphany-core/src/prompt.rs")],
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
..Default::default()
});
state.observations[0].code_refs.push(EpiphanyCodeRef {
path: PathBuf::from("epiphany-core/src/prompt.rs"),
path: PathBuf::from("epiphany-state-model/src/prompt.rs"),
start_line: Some(30),
end_line: Some(40),
symbol: Some("render_epiphany_state".to_string()),
@ -1233,7 +1233,7 @@ mod tests {
#[test]
fn propose_map_update_reuses_existing_deterministic_path_node_without_refs() {
let mut state = state_with_observation("verified");
let node_id = graph_node_id(Path::new("epiphany-core/src/prompt.rs"));
let node_id = graph_node_id(Path::new("epiphany-state-model/src/prompt.rs"));
state.graphs.architecture.nodes.push(EpiphanyGraphNode {
id: node_id.clone(),
title: "Prompt path".to_string(),
@ -1299,7 +1299,7 @@ mod tests {
id: "prompt-renderer".to_string(),
title: "Prompt renderer".to_string(),
purpose: "Render Epiphany state into developer context".to_string(),
code_refs: vec![code_ref("epiphany-core/src/prompt.rs")],
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
..Default::default()
});
@ -1337,7 +1337,7 @@ mod tests {
title: "Prompt renderer".to_string(),
purpose: "Render Epiphany state into developer context".to_string(),
code_refs: vec![EpiphanyCodeRef {
path: PathBuf::from("epiphany-core/src/prompt.rs"),
path: PathBuf::from("epiphany-state-model/src/prompt.rs"),
start_line: Some(100),
end_line: Some(120),
symbol: Some("render_graphs".to_string()),
@ -1412,7 +1412,7 @@ mod tests {
summary: "Live smoke verified prompt renderer Epiphany state injection".to_string(),
source_kind: "smoke".to_string(),
status: "ok".to_string(),
code_refs: vec![code_ref("epiphany-core/src/prompt.rs")],
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
evidence_ids: vec!["ev-strong".to_string()],
});
state.recent_evidence.push(EpiphanyEvidenceRecord {
@ -1420,7 +1420,7 @@ mod tests {
kind: "verification".to_string(),
status: "ok".to_string(),
summary: "Verifier accepted prompt renderer state injection".to_string(),
code_refs: vec![code_ref("epiphany-core/src/prompt.rs")],
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
});
let proposal = propose_map_update(EpiphanyMapProposalInput {
@ -1457,7 +1457,7 @@ mod tests {
summary: "Failed candidate should not be selected".to_string(),
source_kind: "verification".to_string(),
status: "failed".to_string(),
code_refs: vec![code_ref("epiphany-core/src/prompt.rs")],
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
evidence_ids: vec!["ev-failed".to_string()],
});
state.recent_evidence.push(EpiphanyEvidenceRecord {
@ -1465,14 +1465,14 @@ mod tests {
kind: "verification".to_string(),
status: "failed".to_string(),
summary: "Verifier rejected the failed candidate".to_string(),
code_refs: vec![code_ref("epiphany-core/src/prompt.rs")],
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
});
state.observations.push(EpiphanyObservation {
id: "obs-strong".to_string(),
summary: "Live smoke verified prompt renderer Epiphany state injection".to_string(),
source_kind: "smoke".to_string(),
status: "ok".to_string(),
code_refs: vec![code_ref("epiphany-core/src/prompt.rs")],
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
evidence_ids: vec!["ev-strong".to_string()],
});
state.recent_evidence.push(EpiphanyEvidenceRecord {
@ -1480,7 +1480,7 @@ mod tests {
kind: "verification".to_string(),
status: "ok".to_string(),
summary: "Verifier accepted prompt renderer state injection".to_string(),
code_refs: vec![code_ref("epiphany-core/src/prompt.rs")],
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
});
let proposal = propose_map_update(EpiphanyMapProposalInput {
@ -1502,7 +1502,7 @@ mod tests {
fn propose_map_update_auto_selection_prefers_frontier_dirty_path_cluster() {
let mut state = state_with_observation("ok");
state.graph_frontier = Some(EpiphanyGraphFrontier {
dirty_paths: vec![PathBuf::from("epiphany-core/src/prompt.rs")],
dirty_paths: vec![PathBuf::from("epiphany-state-model/src/prompt.rs")],
..Default::default()
});
state.observations.push(EpiphanyObservation {
@ -1537,7 +1537,7 @@ mod tests {
proposal
.graph_frontier
.dirty_paths
.contains(&PathBuf::from("epiphany-core/src/prompt.rs"))
.contains(&PathBuf::from("epiphany-state-model/src/prompt.rs"))
);
assert!(
!proposal
@ -1580,7 +1580,7 @@ mod tests {
id: "prompt-renderer".to_string(),
title: "Prompt renderer".to_string(),
purpose: "Render Epiphany state".to_string(),
code_refs: vec![code_ref("epiphany-core/src/prompt.rs")],
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
..Default::default()
},
],
@ -1619,7 +1619,7 @@ mod tests {
dataflow_node_id: "epiphany-state-fragment".to_string(),
architecture_node_id: "prompt-renderer".to_string(),
relationship: Some("rendered by".to_string()),
code_refs: vec![code_ref("epiphany-core/src/prompt.rs")],
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
}],
};
@ -1739,7 +1739,7 @@ mod tests {
id: "prompt-renderer".to_string(),
title: "Prompt renderer".to_string(),
purpose: "Render Epiphany state into developer context".to_string(),
code_refs: vec![code_ref("epiphany-core/src/prompt.rs")],
code_refs: vec![code_ref("epiphany-state-model/src/prompt.rs")],
..Default::default()
});
state.observations[0]

View file

@ -392,12 +392,12 @@ fn index_workspace_with_config(
.map(|(chunk, vector)| QdrantPointInput {
id: chunk_point_id(&expected_collection, &chunk),
vector,
payload: json!({
"path": normalize_payload_path(&chunk.path),
"line_start": chunk.line_start,
"line_end": chunk.line_end,
"excerpt": chunk.excerpt,
}),
payload: QdrantPointPayload {
path: normalize_payload_path(&chunk.path),
line_start: chunk.line_start,
line_end: chunk.line_end,
excerpt: chunk.excerpt,
},
})
.collect::<Vec<_>>();
qdrant.upsert_points(&expected_collection, &points)?;
@ -1022,7 +1022,16 @@ impl OllamaEmbedder {
struct QdrantPointInput {
id: String,
vector: Vec<f32>,
payload: Value,
payload: QdrantPointPayload,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
struct QdrantPointPayload {
path: String,
line_start: u32,
line_end: u32,
excerpt: String,
}
#[derive(Debug, Deserialize)]
@ -1044,7 +1053,7 @@ struct QdrantQueryResultEnvelope {
struct QdrantQueryPoint {
score: f32,
#[serde(default)]
payload: BTreeMap<String, Value>,
payload: Option<QdrantPointPayload>,
}
#[derive(Debug, Deserialize)]
@ -1386,18 +1395,15 @@ fn search_semantic_chunks(
}
fn map_qdrant_hit_to_result(point: QdrantQueryPoint) -> Option<EpiphanyRetrieveResult> {
let path = point.payload.get("path")?.as_str()?;
let excerpt = point.payload.get("excerpt")?.as_str()?.to_string();
let line_start = point.payload.get("line_start")?.as_u64()? as u32;
let line_end = point.payload.get("line_end")?.as_u64()? as u32;
let payload = point.payload?;
Some(EpiphanyRetrieveResult {
kind: EpiphanyRetrieveResultKind::SemanticChunk,
path: PathBuf::from(path),
path: PathBuf::from(payload.path),
score: point.score,
line_start: Some(line_start),
line_end: Some(line_end),
excerpt: Some(excerpt),
line_start: Some(payload.line_start),
line_end: Some(payload.line_end),
excerpt: Some(payload.excerpt),
})
}

View file

@ -1,9 +1,43 @@
use crate::EpiphanyWorkerLaunchDocument;
use crate::agent_memory::AGENT_MEMORY_TYPE;
use crate::body_gateway::BODY_REPO_ACCESS_GRANT_RECEIPT_SCHEMA_VERSION;
use crate::body_gateway::BODY_REPO_ACCESS_GRANT_RECEIPT_TYPE;
use crate::body_gateway::BODY_REPO_ACCESS_REFUSAL_RECEIPT_SCHEMA_VERSION;
use crate::body_gateway::BODY_REPO_ACCESS_REFUSAL_RECEIPT_TYPE;
use crate::body_gateway::BODY_REPO_ACCESS_REQUEST_SCHEMA_VERSION;
use crate::body_gateway::BODY_REPO_ACCESS_REQUEST_TYPE;
use crate::body_gateway::BODY_REPO_ACCESS_REVIEW_SCHEMA_VERSION;
use crate::body_gateway::BODY_REPO_ACCESS_REVIEW_TYPE;
use crate::body_gateway::BODY_REPO_MUTATION_RECEIPT_SCHEMA_VERSION;
use crate::body_gateway::BODY_REPO_MUTATION_RECEIPT_TYPE;
use crate::body_gateway::BODY_REPO_SNAPSHOT_RECEIPT_SCHEMA_VERSION;
use crate::body_gateway::BODY_REPO_SNAPSHOT_RECEIPT_TYPE;
use crate::eyes_gateway::EYES_EVIDENCE_PACKET_SCHEMA_VERSION;
use crate::eyes_gateway::EYES_EVIDENCE_PACKET_TYPE;
use crate::eyes_gateway::EYES_EVIDENCE_REFUSAL_RECEIPT_SCHEMA_VERSION;
use crate::eyes_gateway::EYES_EVIDENCE_REFUSAL_RECEIPT_TYPE;
use crate::eyes_gateway::EYES_EVIDENCE_REQUEST_SCHEMA_VERSION;
use crate::eyes_gateway::EYES_EVIDENCE_REQUEST_TYPE;
use crate::eyes_gateway::EYES_EVIDENCE_REVIEW_SCHEMA_VERSION;
use crate::eyes_gateway::EYES_EVIDENCE_REVIEW_TYPE;
use crate::eyes_gateway::EYES_SOURCE_LOOKUP_RECEIPT_SCHEMA_VERSION;
use crate::eyes_gateway::EYES_SOURCE_LOOKUP_RECEIPT_TYPE;
use crate::heartbeat_state::HEARTBEAT_STATE_SCHEMA_VERSION;
use crate::heartbeat_state::HEARTBEAT_STATE_TYPE;
use crate::memory_graph::MEMORY_GRAPH_SCHEMA_VERSION;
use crate::memory_graph::MEMORY_GRAPH_TYPE;
use crate::mind_gateway::MIND_GATEWAY_REVIEW_SCHEMA_VERSION;
use crate::mind_gateway::MIND_GATEWAY_REVIEW_TYPE;
use crate::mind_gateway::MIND_STATE_COMMIT_RECEIPT_SCHEMA_VERSION;
use crate::mind_gateway::MIND_STATE_COMMIT_RECEIPT_TYPE;
use crate::mind_gateway::MIND_STATE_EFFECT_PROPOSAL_SCHEMA_VERSION;
use crate::mind_gateway::MIND_STATE_EFFECT_PROPOSAL_TYPE;
use crate::mind_gateway::MIND_STATE_REJECTION_RECEIPT_SCHEMA_VERSION;
use crate::mind_gateway::MIND_STATE_REJECTION_RECEIPT_TYPE;
use crate::mind_gateway::MIND_THOUGHT_SCHEMA_VERSION;
use crate::mind_gateway::MIND_THOUGHT_TYPE;
use crate::mind_gateway::MIND_VERSE_ADOPTION_RECEIPT_SCHEMA_VERSION;
use crate::mind_gateway::MIND_VERSE_ADOPTION_RECEIPT_TYPE;
use crate::state_ledger::STATE_LEDGER_SCHEMA_VERSION;
use crate::state_ledger::STATE_LEDGER_STORE_TYPE;
use crate::thread_state_store::THREAD_STATE_SCHEMA_VERSION;
@ -1387,6 +1421,8 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
vec![RUNTIME_JOB_TYPE],
vec![
"Worker launch requests are typed task-intent documents; runtime jobs are lifecycle receipts, not the source of work intent.",
"Core/coordinator policy owns the launch yes/no; the Epiphany-Codex bridge translates between CultNet-shaped intent and Codex JSON only.",
"Codex-hosted executors may gather host facts and perform side effects after the verdict, with readable receipts.",
],
),
mutation_contract(
@ -1422,6 +1458,254 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
"Job results are evidence records; review and acceptance are separate typed flows.",
],
),
mutation_contract(
MIND_THOUGHT_TYPE,
MIND_THOUGHT_SCHEMA_VERSION,
vec![
CultNetDocumentOperation::Snapshot,
CultNetDocumentOperation::IntentSubmit,
CultNetDocumentOperation::ReceiptWatch,
],
CultNetMutationAuthority::Coordinator,
vec![MIND_THOUGHT_TYPE],
vec![MIND_GATEWAY_REVIEW_TYPE, MIND_STATE_REJECTION_RECEIPT_TYPE],
vec![
"Sub-agent output enters Epiphany as thought, not durable state authority.",
"The Mind contract is the gateway between worker output and persistent state.",
],
),
mutation_contract(
MIND_STATE_EFFECT_PROPOSAL_TYPE,
MIND_STATE_EFFECT_PROPOSAL_SCHEMA_VERSION,
vec![
CultNetDocumentOperation::Snapshot,
CultNetDocumentOperation::IntentSubmit,
CultNetDocumentOperation::ReceiptWatch,
],
CultNetMutationAuthority::Coordinator,
vec![MIND_STATE_EFFECT_PROPOSAL_TYPE],
vec![
MIND_GATEWAY_REVIEW_TYPE,
MIND_STATE_COMMIT_RECEIPT_TYPE,
MIND_STATE_REJECTION_RECEIPT_TYPE,
],
vec![
"Mind is the persistent state guardian: role acceptance, reorientation acceptance, Face Interpreter effects, selfPatch, evidence, scratch, checkpoints, graph changes, and objective changes share this gate.",
"Workers and public Verse ingress propose effects; Mind accepts, refuses, or holds them before any durable state mutation.",
],
),
mutation_contract(
MIND_GATEWAY_REVIEW_TYPE,
MIND_GATEWAY_REVIEW_SCHEMA_VERSION,
vec![
CultNetDocumentOperation::Snapshot,
CultNetDocumentOperation::ReceiptWatch,
],
CultNetMutationAuthority::ReadOnly,
vec![],
vec![],
vec![
"Mind reviews are durable receipts explaining accepted, refused, or held state effects.",
],
),
mutation_contract(
MIND_STATE_COMMIT_RECEIPT_TYPE,
MIND_STATE_COMMIT_RECEIPT_SCHEMA_VERSION,
vec![
CultNetDocumentOperation::Snapshot,
CultNetDocumentOperation::ReceiptWatch,
],
CultNetMutationAuthority::ReadOnly,
vec![],
vec![],
vec![
"A commit receipt is proof that Mind, not the worker, admitted a proposed effect into durable state.",
],
),
mutation_contract(
MIND_STATE_REJECTION_RECEIPT_TYPE,
MIND_STATE_REJECTION_RECEIPT_SCHEMA_VERSION,
vec![
CultNetDocumentOperation::Snapshot,
CultNetDocumentOperation::ReceiptWatch,
],
CultNetMutationAuthority::ReadOnly,
vec![],
vec![],
vec![
"A rejection receipt preserves why a thought or state effect was refused without mutating the Mind.",
],
),
mutation_contract(
MIND_VERSE_ADOPTION_RECEIPT_TYPE,
MIND_VERSE_ADOPTION_RECEIPT_SCHEMA_VERSION,
vec![
CultNetDocumentOperation::Snapshot,
CultNetDocumentOperation::ReceiptWatch,
],
CultNetMutationAuthority::ReadOnly,
vec![],
vec![],
vec![
"Foreign or public Verse material is thought weather until local Mind emits an adoption receipt.",
"The global Verse never receives private state authority by being interesting.",
],
),
mutation_contract(
BODY_REPO_ACCESS_REQUEST_TYPE,
BODY_REPO_ACCESS_REQUEST_SCHEMA_VERSION,
vec![
CultNetDocumentOperation::Snapshot,
CultNetDocumentOperation::IntentSubmit,
CultNetDocumentOperation::ReceiptWatch,
],
CultNetMutationAuthority::Coordinator,
vec![BODY_REPO_ACCESS_REQUEST_TYPE],
vec![
BODY_REPO_ACCESS_REVIEW_TYPE,
BODY_REPO_ACCESS_GRANT_RECEIPT_TYPE,
BODY_REPO_ACCESS_REFUSAL_RECEIPT_TYPE,
],
vec![
"Body is the repository access guardian: reads, indexing, edits, commands, and bridge operations must be requested through this contract.",
"Hands mutates only after a scoped Body grant; Eyes inspects only after a scoped Body read/index grant.",
],
),
mutation_contract(
BODY_REPO_ACCESS_REVIEW_TYPE,
BODY_REPO_ACCESS_REVIEW_SCHEMA_VERSION,
vec![
CultNetDocumentOperation::Snapshot,
CultNetDocumentOperation::ReceiptWatch,
],
CultNetMutationAuthority::ReadOnly,
vec![],
vec![],
vec![
"Body reviews explain granted/refused repo paths, operations, commands, and bridge surfaces.",
],
),
mutation_contract(
BODY_REPO_ACCESS_GRANT_RECEIPT_TYPE,
BODY_REPO_ACCESS_GRANT_RECEIPT_SCHEMA_VERSION,
vec![
CultNetDocumentOperation::Snapshot,
CultNetDocumentOperation::ReceiptWatch,
],
CultNetMutationAuthority::ReadOnly,
vec![],
vec![],
vec!["A Body grant receipt scopes a permitted repo touch."],
),
mutation_contract(
BODY_REPO_ACCESS_REFUSAL_RECEIPT_TYPE,
BODY_REPO_ACCESS_REFUSAL_RECEIPT_SCHEMA_VERSION,
vec![
CultNetDocumentOperation::Snapshot,
CultNetDocumentOperation::ReceiptWatch,
],
CultNetMutationAuthority::ReadOnly,
vec![],
vec![],
vec!["A Body refusal receipt preserves why repo access was denied."],
),
mutation_contract(
BODY_REPO_SNAPSHOT_RECEIPT_TYPE,
BODY_REPO_SNAPSHOT_RECEIPT_SCHEMA_VERSION,
vec![
CultNetDocumentOperation::Snapshot,
CultNetDocumentOperation::ReceiptWatch,
],
CultNetMutationAuthority::ReadOnly,
vec![],
vec![],
vec!["Repo snapshots are evidence projections from Body-gated access."],
),
mutation_contract(
BODY_REPO_MUTATION_RECEIPT_TYPE,
BODY_REPO_MUTATION_RECEIPT_SCHEMA_VERSION,
vec![
CultNetDocumentOperation::Snapshot,
CultNetDocumentOperation::ReceiptWatch,
],
CultNetMutationAuthority::ReadOnly,
vec![],
vec![],
vec![
"Repo mutation receipts prove Body granted the substrate touch before Hands changed files or ran repo-affecting commands.",
],
),
mutation_contract(
EYES_EVIDENCE_REQUEST_TYPE,
EYES_EVIDENCE_REQUEST_SCHEMA_VERSION,
vec![
CultNetDocumentOperation::Snapshot,
CultNetDocumentOperation::IntentSubmit,
CultNetDocumentOperation::ReceiptWatch,
],
CultNetMutationAuthority::Coordinator,
vec![EYES_EVIDENCE_REQUEST_TYPE],
vec![
EYES_EVIDENCE_REVIEW_TYPE,
EYES_SOURCE_LOOKUP_RECEIPT_TYPE,
EYES_EVIDENCE_PACKET_TYPE,
EYES_EVIDENCE_REFUSAL_RECEIPT_TYPE,
],
vec![
"Eyes is the evidence ingress guardian: source-grounded claims, provenance, uncertainty, and evidence packets enter through this contract.",
"Body grants substrate access; Eyes decides what was actually inspected and what other organs may cite.",
],
),
mutation_contract(
EYES_EVIDENCE_REVIEW_TYPE,
EYES_EVIDENCE_REVIEW_SCHEMA_VERSION,
vec![
CultNetDocumentOperation::Snapshot,
CultNetDocumentOperation::ReceiptWatch,
],
CultNetMutationAuthority::ReadOnly,
vec![],
vec![],
vec!["Eyes reviews explain whether a claim is source-grounded, uncertain, or refused."],
),
mutation_contract(
EYES_SOURCE_LOOKUP_RECEIPT_TYPE,
EYES_SOURCE_LOOKUP_RECEIPT_SCHEMA_VERSION,
vec![
CultNetDocumentOperation::Snapshot,
CultNetDocumentOperation::ReceiptWatch,
],
CultNetMutationAuthority::ReadOnly,
vec![],
vec![],
vec!["Source lookup receipts prove what was searched or inspected under a Body grant."],
),
mutation_contract(
EYES_EVIDENCE_PACKET_TYPE,
EYES_EVIDENCE_PACKET_SCHEMA_VERSION,
vec![
CultNetDocumentOperation::Snapshot,
CultNetDocumentOperation::ReceiptWatch,
],
CultNetMutationAuthority::ReadOnly,
vec![],
vec![],
vec![
"Evidence packets carry provenance, uncertainty, and source refs for the other organs.",
],
),
mutation_contract(
EYES_EVIDENCE_REFUSAL_RECEIPT_TYPE,
EYES_EVIDENCE_REFUSAL_RECEIPT_SCHEMA_VERSION,
vec![
CultNetDocumentOperation::Snapshot,
CultNetDocumentOperation::ReceiptWatch,
],
CultNetMutationAuthority::ReadOnly,
vec![],
vec![],
vec!["Evidence refusal receipts preserve why Eyes would not certify a claim."],
),
mutation_contract(
RUNTIME_EVENT_TYPE,
RUNTIME_SPINE_SCHEMA_VERSION,
@ -1487,7 +1771,7 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
vec!["epiphany.agent_memory_intent.v0"],
vec!["epiphany.swarm_control_receipt.v0"],
vec![
"Sub-agents request memory mutations; the coordinator accepts, rejects, or explains the refusal.",
"Sub-agents request memory mutations; the coordinator carries the typed intent, and Mind accepts, rejects, or explains durable-state admission.",
],
),
mutation_contract(
@ -1501,11 +1785,13 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
CultNetMutationAuthority::Coordinator,
vec![
"epiphany.heartbeat_pump_intent.v0",
"epiphany.heartbeat_heat_intent.v0",
"epiphany.circadian_rhythm_intent.v0",
],
vec!["epiphany.swarm_control_receipt.v0"],
vec![
"Aquarium controls heartbeat and circadian rhythm through typed intents, not blind state replacement.",
"Initiative heat is heartbeat policy: global, group, role, and agent tempo changes enter through the heartbeat heat intent.",
],
),
mutation_contract(
@ -1545,7 +1831,7 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
SURFACE_SCENE_TYPE,
SCENE_SURFACE_SCHEMA_VERSION,
vec![
"Operator-safe scene reflection mirrored from the thread/epiphany/view scene lens.",
"Operator-safe scene reflection over typed Epiphany state.",
"Aquarium should read this before offering live coordination actions.",
],
),
@ -1553,7 +1839,7 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
SURFACE_FRESHNESS_TYPE,
FRESHNESS_SURFACE_SCHEMA_VERSION,
vec![
"Freshness reflection mirrored from thread/epiphany/freshness.",
"Freshness reflection over retrieval, watcher, and graph staleness signals.",
"Use this to visualize retrieval and graph staleness without inventing a hidden refresh daemon.",
],
),
@ -1561,7 +1847,7 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
SURFACE_CONTEXT_TYPE,
CONTEXT_SURFACE_SCHEMA_VERSION,
vec![
"Targeted graph, frontier, checkpoint, observation, and evidence context mirrored from thread/epiphany/context.",
"Targeted graph, frontier, checkpoint, observation, and evidence context shard over typed Epiphany state.",
"Aquarium should inspect bounded state shards here instead of scraping state blobs by superstition.",
],
),
@ -1569,7 +1855,7 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
SURFACE_GRAPH_QUERY_TYPE,
GRAPH_QUERY_SURFACE_SCHEMA_VERSION,
vec![
"Bounded graph traversal mirrored from thread/epiphany/graphQuery.",
"Bounded graph traversal over typed architecture/dataflow graph state.",
"Use this for architecture/dataflow inspection and frontier neighborhoods without mutating state.",
],
),
@ -1577,7 +1863,7 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
SURFACE_PRESSURE_TYPE,
PRESSURE_SURFACE_SCHEMA_VERSION,
vec![
"Current context pressure and compaction posture mirrored from thread/epiphany/pressure.",
"Current context pressure and compaction posture derived from typed pressure inputs.",
"This is a read-only warning surface, not a backdoor to force state mutation.",
],
),
@ -1593,15 +1879,16 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
"epiphany.reorient_result_surface.v0",
],
vec![
"Reorientation policy is mirrored from thread/epiphany/reorient.",
"Reorientation policy is the typed resume/regather verdict surface.",
"Launch and acceptance stay review-gated through explicit typed intents.",
"The transport may carry a reorient launch intent and receipt, but the resume/regather verdict belongs to core.",
],
),
read_only_surface_contract(
SURFACE_CRRC_TYPE,
CRRC_SURFACE_SCHEMA_VERSION,
vec![
"CRRC recommendation surface mirrored from thread/epiphany/crrc.",
"CRRC recommendation surface over continuity, pressure, and reorientation signals.",
"Use this to understand continuity pressure without letting CRRC seize authority.",
],
),
@ -1614,8 +1901,9 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
],
vec!["epiphany.swarm_control_receipt.v0"],
vec![
"Job reflection mirrored from the thread/epiphany/view jobs lens, with launch/interrupt affordances on jobLaunch and jobInterrupt.",
"Job reflection over typed job bindings and runtime-spine lifecycle receipts.",
"Heartbeat/runtime-spine owns activation; callers submit typed intents and watch receipts.",
"Launch receipts must name decisionOwner, transportRole, and any hostExecutorRole so transport never grows an unreadable second opinion.",
],
),
coordinator_surface_contract(
@ -1624,7 +1912,7 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
vec!["epiphany.role_launch_intent.v0"],
vec!["epiphany.swarm_control_receipt.v0"],
vec![
"Role ownership mirrored from the thread/epiphany/view roles lens, with launch affordances on roleLaunch.",
"Role ownership surface for fixed Epiphany lanes and launch affordances.",
"Treat this as the discoverable lane catalog for Aquarium.",
],
),
@ -1634,7 +1922,7 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
vec!["epiphany.role_accept_intent.v0"],
vec!["epiphany.swarm_control_receipt.v0"],
vec![
"Role findings are read through thread/epiphany/roleResult and accepted through roleAccept.",
"Role findings are typed review surfaces accepted through explicit role acceptance intents.",
"Semantic findings remain explicitly review-gated.",
],
),
@ -1644,7 +1932,7 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
vec!["epiphany.reorient_accept_intent.v0"],
vec!["epiphany.swarm_control_receipt.v0"],
vec![
"Completed reorientation findings are read through thread/epiphany/reorientResult and accepted through reorientAccept.",
"Completed reorientation findings are typed review surfaces accepted through explicit reorientation acceptance intents.",
],
),
coordinator_surface_contract(
@ -1656,7 +1944,7 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
],
vec!["epiphany.swarm_control_receipt.v0"],
vec![
"Planning projection is mirrored from the thread/epiphany/view planning lens.",
"Planning projection over captures, backlog, roadmap streams, and Objective Drafts.",
"Backlog, captures, and Objective Drafts are planning state until explicit adoption.",
],
),
@ -1664,7 +1952,7 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
SURFACE_COORDINATOR_TYPE,
COORDINATOR_SURFACE_SCHEMA_VERSION,
vec![
"Fixed-lane recommendation surface mirrored from thread/epiphany/coordinator.",
"Fixed-lane recommendation surface derived from typed role, pressure, reorientation, and result signals.",
"Aquarium should treat this as the primary action oracle, not invent its own scheduler.",
],
),
@ -1682,7 +1970,7 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
"epiphany.character_turn_packet.v0",
],
vec![
"Face bubble, draft, and Discord persona affordances are mirrored from epiphany-face-discord and epiphany-character-loop.",
"Face bubble, draft, and Discord persona affordances are projected from typed Face and character-loop artifacts.",
"Humans talk to Face; sealed inner thoughts stay behind the projection boundary.",
],
),
@ -1690,7 +1978,7 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
SURFACE_VOID_MEMORY_TYPE,
VOID_MEMORY_SURFACE_SCHEMA_VERSION,
vec![
"Void-derived memory status/search/context availability is mirrored from epiphany-void-memory.",
"Void-derived memory status/search/context availability is projected from the typed Void memory bridge.",
"This is an inspection surface for the memory organs, not a license to bypass typed Epiphany state.",
],
),
@ -1706,7 +1994,7 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
"epiphany.repo_initialization_record.v0",
],
vec![
"Repo birth/startup status is mirrored from epiphany-repo-personality startup and accept-init.",
"Repo birth/startup status is projected from typed repo-personality startup and accept-init receipts.",
"Birth specialists are startup-only and remain outside the heartbeat lane system.",
],
),
@ -1719,7 +2007,7 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
"epiphany.repo_initialization_record.v0",
],
vec![
"Startup-only birth runner plan/run affordances are mirrored from epiphany-repo-birth-runner.",
"Startup-only birth runner plan/run affordances are projected from typed birth-runner receipts.",
"Aquarium should review birth artifacts and accept them explicitly instead of growing a hidden wizard.",
],
),
@ -1733,7 +2021,7 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
],
vec!["epiphany.swarm_control_receipt.v0"],
vec![
"Rider bridge affordances are mirrored from epiphany-rider-bridge status/context/open-ref.",
"Rider bridge affordances are projected from typed Rider bridge artifacts.",
"Use this surface for source/IDE inspection instead of scraping local artifacts by convention.",
],
),
@ -1748,7 +2036,7 @@ fn epiphany_mutation_contracts() -> Vec<CultNetDocumentMutationContract> {
],
vec!["epiphany.swarm_control_receipt.v0"],
vec![
"Unity bridge affordances are mirrored from epiphany-unity-bridge inspect/probe/check-compilation/run-tests.",
"Unity bridge affordances are projected from typed Unity bridge artifacts.",
"Runtime/editor truth must come through the pinned bridge and its receipts, not wishful source reading.",
],
),
@ -2154,6 +2442,77 @@ mod tests {
.operations
.contains(&CultNetDocumentOperation::IntentSubmit)
);
assert!(
heartbeat_contract
.intent_document_types
.as_ref()
.is_some_and(|items| items
.iter()
.any(|item| item == "epiphany.heartbeat_heat_intent.v0"))
);
let mind_state_contract = contracts
.iter()
.find(|contract| contract.document_type == MIND_STATE_EFFECT_PROPOSAL_TYPE)
.expect("Mind state-effect proposal should advertise a mutation contract");
assert_eq!(
mind_state_contract.authority,
CultNetMutationAuthority::Coordinator
);
assert!(
mind_state_contract
.receipt_document_types
.as_ref()
.is_some_and(|items| items
.iter()
.any(|item| item == MIND_STATE_COMMIT_RECEIPT_TYPE))
);
assert!(mind_state_contract.notes.as_ref().is_some_and(|notes| {
notes
.iter()
.any(|note| note.contains("persistent state guardian"))
}));
let body_repo_contract = contracts
.iter()
.find(|contract| contract.document_type == BODY_REPO_ACCESS_REQUEST_TYPE)
.expect("Body repo access should advertise a mutation contract");
assert_eq!(
body_repo_contract.authority,
CultNetMutationAuthority::Coordinator
);
assert!(
body_repo_contract
.receipt_document_types
.as_ref()
.is_some_and(|items| items
.iter()
.any(|item| item == BODY_REPO_ACCESS_GRANT_RECEIPT_TYPE))
);
assert!(body_repo_contract.notes.as_ref().is_some_and(|notes| {
notes
.iter()
.any(|note| note.contains("repository access guardian"))
}));
let eyes_evidence_contract = contracts
.iter()
.find(|contract| contract.document_type == EYES_EVIDENCE_REQUEST_TYPE)
.expect("Eyes evidence request should advertise a mutation contract");
assert_eq!(
eyes_evidence_contract.authority,
CultNetMutationAuthority::Coordinator
);
assert!(
eyes_evidence_contract
.receipt_document_types
.as_ref()
.is_some_and(|items| items
.iter()
.any(|item| item == EYES_EVIDENCE_PACKET_TYPE))
);
assert!(eyes_evidence_contract.notes.as_ref().is_some_and(|notes| {
notes
.iter()
.any(|note| note.contains("evidence ingress guardian"))
}));
let coordinator_contract = contracts
.iter()
.find(|contract| contract.document_type == SURFACE_COORDINATOR_TYPE)
@ -2260,10 +2619,42 @@ mod tests {
schema.document_type.as_deref() == Some("epiphany.role_launch_intent.v0")
&& schema.schema_version.as_deref() == Some("epiphany.role_launch_intent.v0")
}));
assert!(schemas.iter().any(|schema| {
schema.document_type.as_deref() == Some("epiphany.heartbeat_initiative_heat.v0")
&& schema.schema_version.as_deref() == Some("epiphany.heartbeat_initiative_heat.v0")
}));
assert!(schemas.iter().any(|schema| {
schema.document_type.as_deref() == Some("epiphany.heartbeat_heat_intent.v0")
&& schema.schema_version.as_deref() == Some("epiphany.heartbeat_heat_intent.v0")
}));
assert!(schemas.iter().any(|schema| {
schema.document_type.as_deref() == Some("epiphany.swarm_control_receipt.v0")
&& schema.schema_version.as_deref() == Some("epiphany.swarm_control_receipt.v0")
}));
let receipt_schema_path =
epiphany_schema_root().join("epiphany.swarm-control-receipt.schema.json");
let receipt_schema: serde_json::Value =
serde_json::from_str(&fs::read_to_string(receipt_schema_path)?)?;
let required = receipt_schema["required"]
.as_array()
.expect("receipt schema should list required fields");
assert!(required.iter().any(|field| field == "decisionOwner"));
assert!(required.iter().any(|field| field == "transportRole"));
assert!(
receipt_schema["properties"]["decisionOwner"]["description"]
.as_str()
.is_some_and(|description| description.contains("epiphany-core"))
);
assert!(
receipt_schema["properties"]["transportRole"]["description"]
.as_str()
.is_some_and(|description| description.contains("must not make"))
);
assert!(
receipt_schema["properties"]["hostExecutorRole"]["description"]
.as_str()
.is_some_and(|description| description.contains("gathered facts"))
);
assert!(schemas.iter().any(|schema| {
schema.document_type.as_deref() == Some(OPENAI_MODEL_REQUEST_TYPE)
&& schema.schema_version.as_deref() == Some(OPENAI_MODEL_REQUEST_SCHEMA_VERSION)

View file

@ -6,7 +6,6 @@ use cultcache_rs::SingleFileMessagePackBackingStore;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
use std::collections::BTreeMap;
use std::path::Path;
pub const STATE_LEDGER_STORE_TYPE: &str = "epiphany.state_ledger";
@ -22,8 +21,6 @@ pub struct EpiphanyStateLedgerEntry {
pub branches: Vec<EpiphanyBranchRecord>,
#[cultcache(key = 2)]
pub evidence: Vec<EpiphanyLedgerEvidenceRecord>,
#[cultcache(key = 3, default)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@ -33,8 +30,6 @@ pub struct EpiphanyBranchRecord {
pub status: String,
pub artifacts: Vec<String>,
pub notes: String,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@ -46,8 +41,6 @@ pub struct EpiphanyLedgerEvidenceRecord {
pub note: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
pub fn state_ledger_status(store_path: impl AsRef<Path>) -> Result<Value> {
@ -178,7 +171,6 @@ fn default_state_ledger() -> EpiphanyStateLedgerEntry {
schema_version: STATE_LEDGER_SCHEMA_VERSION.to_string(),
branches: Vec::new(),
evidence: Vec::new(),
extra: BTreeMap::new(),
}
}
@ -228,7 +220,6 @@ mod tests {
status: "active".to_string(),
artifacts: vec!["state/map.yaml".to_string()],
notes: "test".to_string(),
extra: BTreeMap::new(),
},
)?;
let entry = load_state_ledger(&store)?;
@ -242,7 +233,6 @@ mod tests {
status: "ok".to_string(),
note: "Native evidence append works.".to_string(),
branch: Some("main".to_string()),
extra: BTreeMap::new(),
},
)?;
assert_eq!(load_state_ledger(&store)?.evidence.len(), 1);

View file

@ -19,6 +19,27 @@ use epiphany_state_model::EpiphanySubgoal;
use epiphany_state_model::EpiphanyThreadState;
use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EpiphanyStateUpdatedField {
Objective,
ActiveSubgoalId,
Subgoals,
Invariants,
Graphs,
GraphFrontier,
GraphCheckpoint,
Scratch,
InvestigationCheckpoint,
JobBindings,
AcceptanceReceipts,
RuntimeLinks,
Observations,
Evidence,
Churn,
Mode,
Planning,
}
#[derive(Debug, Clone, Default)]
pub struct EpiphanyStateUpdate {
pub expected_revision: Option<u64>,

View file

@ -1,8 +1,19 @@
use super::EpiphanyCrrcAction;
use super::EpiphanyCrrcSceneAction;
use super::EpiphanyCrrcStateStatus;
use super::EpiphanyPressure;
use super::EpiphanyPressureLevel;
use super::EpiphanyReorientAction;
use super::EpiphanyReorientFindingInterpretation;
use super::EpiphanyRoleBoardLane;
use super::EpiphanyRoleFindingInterpretation;
use epiphany_state_model::EpiphanyThreadState;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyCoordinatorRoleId {
Implementation,
Imagination,
@ -11,7 +22,8 @@ pub enum EpiphanyCoordinatorRoleId {
Reorientation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyCoordinatorRoleStatus {
Ready,
Needed,
@ -23,7 +35,8 @@ pub enum EpiphanyCoordinatorRoleStatus {
Completed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyCoordinatorRoleResultStatus {
MissingState,
MissingBinding,
@ -36,7 +49,8 @@ pub enum EpiphanyCoordinatorRoleResultStatus {
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyCoordinatorAction {
PrepareCheckpoint,
CompactRehydrateReorient,
@ -51,7 +65,8 @@ pub enum EpiphanyCoordinatorAction {
ContinueImplementation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyCoordinatorSceneAction {
Update,
Reorient,
@ -62,26 +77,42 @@ pub enum EpiphanyCoordinatorSceneAction {
RoleResult,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyCoordinatorAutomationAction {
None,
CompactRehydrateReorient,
LaunchReorientWorker,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyCoordinatorCrrcRecommendation {
pub action: EpiphanyCrrcAction,
pub recommended_scene_action: Option<EpiphanyCoordinatorSceneAction>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyCoordinatorSignals {
pub modeling_result_status: EpiphanyCoordinatorRoleResultStatus,
pub verification_result_status: EpiphanyCoordinatorRoleResultStatus,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyCoordinatorSourceSignals {
pub pressure_level: EpiphanyPressureLevel,
pub should_prepare_compaction: bool,
pub reorient_action: EpiphanyReorientAction,
pub crrc_action: EpiphanyCrrcAction,
pub modeling_result_status: EpiphanyCoordinatorRoleResultStatus,
pub verification_result_status: EpiphanyCoordinatorRoleResultStatus,
pub reorient_result_status: super::EpiphanyCrrcResultStatus,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyCoordinatorRoleLane {
pub id: EpiphanyCoordinatorRoleId,
pub status: EpiphanyCoordinatorRoleStatus,
@ -107,7 +138,8 @@ pub struct EpiphanyCoordinatorInput {
pub reorient_finding_accepted: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyCoordinatorDecision {
pub action: EpiphanyCoordinatorAction,
pub target_role: Option<EpiphanyCoordinatorRoleId>,
@ -117,6 +149,50 @@ pub struct EpiphanyCoordinatorDecision {
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EpiphanyCoordinatorStatusInput {
pub state_status: EpiphanyCrrcStateStatus,
pub checkpoint_present: bool,
pub pressure: EpiphanyPressure,
pub recommendation: super::EpiphanyCrrcRecommendation,
pub roles: Vec<EpiphanyRoleBoardLane>,
pub reorient_action: EpiphanyReorientAction,
pub modeling_result_status: EpiphanyCoordinatorRoleResultStatus,
pub verification_result_status: EpiphanyCoordinatorRoleResultStatus,
pub reorient_result_status: super::EpiphanyCrrcResultStatus,
pub modeling_result_accepted: bool,
pub modeling_result_reviewable: bool,
pub modeling_result_accepted_after_verification: bool,
pub implementation_evidence_after_verification: bool,
pub verification_result_cites_implementation_evidence: bool,
pub verification_result_covers_current_modeling: bool,
pub verification_result_accepted: bool,
pub verification_result_allows_implementation: bool,
pub verification_result_needs_evidence: bool,
pub reorient_finding_accepted: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EpiphanyCoordinatorStatus {
pub decision: EpiphanyCoordinatorDecision,
pub source_signals: EpiphanyCoordinatorSourceSignals,
pub roles: Vec<EpiphanyRoleBoardLane>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct EpiphanyCoordinatorFindingSignals {
pub modeling_result_accepted: bool,
pub modeling_result_reviewable: bool,
pub modeling_result_accepted_after_verification: bool,
pub implementation_evidence_after_verification: bool,
pub verification_result_cites_implementation_evidence: bool,
pub verification_result_covers_current_modeling: bool,
pub verification_result_accepted: bool,
pub verification_result_allows_implementation: bool,
pub verification_result_needs_evidence: bool,
pub reorient_finding_accepted: bool,
}
pub fn crrc_scene_action_to_coordinator_scene_action(
action: EpiphanyCrrcSceneAction,
) -> EpiphanyCoordinatorSceneAction {
@ -129,6 +205,349 @@ pub fn crrc_scene_action_to_coordinator_scene_action(
}
}
pub fn derive_coordinator_status(
input: EpiphanyCoordinatorStatusInput,
) -> EpiphanyCoordinatorStatus {
let source_signals = EpiphanyCoordinatorSourceSignals {
pressure_level: input.pressure.level,
should_prepare_compaction: input.pressure.should_prepare_compaction,
reorient_action: input.reorient_action,
crrc_action: input.recommendation.action,
modeling_result_status: input.modeling_result_status,
verification_result_status: input.verification_result_status,
reorient_result_status: input.reorient_result_status,
};
let coordinator_roles = input
.roles
.iter()
.map(coordinator_role_lane_from_role_board)
.collect();
let decision = recommend_coordinator_action(EpiphanyCoordinatorInput {
state_status: input.state_status,
checkpoint_present: input.checkpoint_present,
should_prepare_compaction: input.pressure.should_prepare_compaction,
recommendation: EpiphanyCoordinatorCrrcRecommendation {
action: input.recommendation.action,
recommended_scene_action: input
.recommendation
.recommended_scene_action
.map(crrc_scene_action_to_coordinator_scene_action),
},
roles: coordinator_roles,
signals: EpiphanyCoordinatorSignals {
modeling_result_status: input.modeling_result_status,
verification_result_status: input.verification_result_status,
},
modeling_result_accepted: input.modeling_result_accepted,
modeling_result_reviewable: input.modeling_result_reviewable,
modeling_result_accepted_after_verification: input
.modeling_result_accepted_after_verification,
implementation_evidence_after_verification: input
.implementation_evidence_after_verification,
verification_result_cites_implementation_evidence: input
.verification_result_cites_implementation_evidence,
verification_result_covers_current_modeling: input
.verification_result_covers_current_modeling,
verification_result_accepted: input.verification_result_accepted,
verification_result_allows_implementation: input.verification_result_allows_implementation,
verification_result_needs_evidence: input.verification_result_needs_evidence,
reorient_finding_accepted: input.reorient_finding_accepted,
});
EpiphanyCoordinatorStatus {
decision,
source_signals,
roles: input.roles,
}
}
pub fn derive_coordinator_finding_signals(
state: Option<&EpiphanyThreadState>,
modeling_finding: Option<&EpiphanyRoleFindingInterpretation>,
verification_finding: Option<&EpiphanyRoleFindingInterpretation>,
reorient_finding: Option<&EpiphanyReorientFindingInterpretation>,
) -> EpiphanyCoordinatorFindingSignals {
let modeling_result_accepted = modeling_finding.as_ref().is_some_and(|finding| {
state.is_some_and(|state| {
role_finding_already_accepted(state, EpiphanyCoordinatorRoleId::Modeling, finding)
})
});
let modeling_result_reviewable =
modeling_finding.is_some_and(modeling_finding_has_reviewable_state_patch);
let verification_result_accepted = verification_finding.as_ref().is_some_and(|finding| {
state.is_some_and(|state| {
role_finding_already_accepted(state, EpiphanyCoordinatorRoleId::Verification, finding)
})
});
let verification_result_covers_current_modeling = state.is_none_or(|state| {
verification_finding_covers_current_modeling(
state,
modeling_result_accepted,
modeling_finding,
verification_finding,
)
});
let modeling_result_accepted_after_verification = state.is_some_and(|state| {
role_finding_accepted_after(
state,
EpiphanyCoordinatorRoleId::Modeling,
modeling_finding,
EpiphanyCoordinatorRoleId::Verification,
verification_finding,
)
});
let implementation_evidence_after_verification = state.is_some_and(|state| {
implementation_evidence_after_role_finding(
state,
EpiphanyCoordinatorRoleId::Verification,
verification_finding,
)
});
let verification_result_cites_implementation_evidence = state.is_some_and(|state| {
role_finding_cites_implementation_evidence(state, verification_finding)
});
let verification_result_allows_implementation = verification_result_accepted
&& verification_finding.is_some_and(verification_finding_allows_implementation);
let verification_result_needs_evidence = verification_result_accepted
&& verification_finding.is_some_and(verification_finding_needs_evidence);
let reorient_finding_accepted = reorient_finding.as_ref().is_some_and(|finding| {
state.is_some_and(|state| reorient_finding_already_accepted(state, finding))
});
EpiphanyCoordinatorFindingSignals {
modeling_result_accepted,
modeling_result_reviewable,
modeling_result_accepted_after_verification,
implementation_evidence_after_verification,
verification_result_cites_implementation_evidence,
verification_result_covers_current_modeling,
verification_result_accepted,
verification_result_allows_implementation,
verification_result_needs_evidence,
reorient_finding_accepted,
}
}
pub fn reorient_finding_already_accepted(
state: &EpiphanyThreadState,
finding: &EpiphanyReorientFindingInterpretation,
) -> bool {
if let Some(result_id) = finding.runtime_result_id.clone()
&& state.acceptance_receipts.iter().any(|receipt| {
receipt.result_id == result_id
&& receipt.status == "accepted"
&& receipt.surface == "reorientAccept"
})
{
return true;
}
false
}
fn modeling_finding_has_reviewable_state_patch(
finding: &EpiphanyRoleFindingInterpretation,
) -> bool {
finding
.state_patch
.as_ref()
.is_some_and(|patch| super::modeling_role_state_patch_policy_errors(patch).is_empty())
}
fn role_finding_already_accepted(
state: &EpiphanyThreadState,
role_id: EpiphanyCoordinatorRoleId,
finding: &EpiphanyRoleFindingInterpretation,
) -> bool {
role_finding_accepted_index(state, role_id, finding).is_some()
}
fn role_finding_accepted_evidence_id(
state: &EpiphanyThreadState,
role_id: EpiphanyCoordinatorRoleId,
finding: &EpiphanyRoleFindingInterpretation,
) -> Option<String> {
role_finding_acceptance_receipt(state, role_id, finding)
.and_then(|receipt| receipt.accepted_evidence_id.clone())
}
fn verification_finding_covers_current_modeling(
state: &EpiphanyThreadState,
modeling_result_accepted: bool,
modeling_finding: Option<&EpiphanyRoleFindingInterpretation>,
verification_finding: Option<&EpiphanyRoleFindingInterpretation>,
) -> bool {
if !modeling_result_accepted {
return true;
}
let Some(modeling_finding) = modeling_finding else {
return true;
};
let Some(verification_finding) = verification_finding else {
return false;
};
let mut modeling_evidence_ids: HashSet<String> =
modeling_finding.evidence_ids.iter().cloned().collect();
if let Some(accepted_id) = role_finding_accepted_evidence_id(
state,
EpiphanyCoordinatorRoleId::Modeling,
modeling_finding,
) {
modeling_evidence_ids.insert(accepted_id);
}
if modeling_evidence_ids.is_empty() {
return true;
}
verification_finding
.evidence_ids
.iter()
.any(|id| modeling_evidence_ids.contains(id))
}
fn role_finding_accepted_after(
state: &EpiphanyThreadState,
later_role_id: EpiphanyCoordinatorRoleId,
later: Option<&EpiphanyRoleFindingInterpretation>,
earlier_role_id: EpiphanyCoordinatorRoleId,
earlier: Option<&EpiphanyRoleFindingInterpretation>,
) -> bool {
let Some(later) = later else {
return false;
};
let Some(later_index) = role_finding_accepted_order_index(state, later_role_id, later) else {
return false;
};
let Some(earlier) = earlier else {
return true;
};
let Some(earlier_index) = role_finding_accepted_order_index(state, earlier_role_id, earlier)
else {
return true;
};
later_index < earlier_index
}
fn implementation_evidence_after_role_finding(
state: &EpiphanyThreadState,
role_id: EpiphanyCoordinatorRoleId,
earlier: Option<&EpiphanyRoleFindingInterpretation>,
) -> bool {
let earlier_index =
earlier.and_then(|finding| role_finding_accepted_index(state, role_id, finding));
state
.recent_evidence
.iter()
.enumerate()
.find(|(index, evidence)| {
evidence.kind == "implementation-audit"
&& earlier_index.is_none_or(|earlier_index| *index < earlier_index)
})
.is_some_and(|(_, evidence)| evidence.status == "ok")
}
fn role_finding_cites_implementation_evidence(
state: &EpiphanyThreadState,
finding: Option<&EpiphanyRoleFindingInterpretation>,
) -> bool {
let Some(finding) = finding else {
return false;
};
finding.evidence_ids.iter().any(|id| {
state.recent_evidence.iter().any(|evidence| {
evidence.id == *id && evidence.kind == "implementation-audit" && evidence.status == "ok"
})
})
}
fn role_finding_accepted_index(
state: &EpiphanyThreadState,
role_id: EpiphanyCoordinatorRoleId,
finding: &EpiphanyRoleFindingInterpretation,
) -> Option<usize> {
if let Some(receipt) = role_finding_acceptance_receipt(state, role_id, finding) {
return receipt
.accepted_evidence_id
.as_ref()
.and_then(|id| {
state
.recent_evidence
.iter()
.position(|evidence| evidence.id == *id)
})
.or(Some(0));
}
None
}
fn role_finding_accepted_order_index(
state: &EpiphanyThreadState,
role_id: EpiphanyCoordinatorRoleId,
finding: &EpiphanyRoleFindingInterpretation,
) -> Option<usize> {
role_finding_acceptance_receipt_index(state, role_id, finding)
}
fn role_finding_acceptance_receipt<'a>(
state: &'a EpiphanyThreadState,
role_id: EpiphanyCoordinatorRoleId,
finding: &EpiphanyRoleFindingInterpretation,
) -> Option<&'a epiphany_state_model::EpiphanyAcceptanceReceipt> {
let result_id = finding.runtime_result_id.clone()?;
state.acceptance_receipts.iter().find(|receipt| {
receipt.result_id == result_id
&& receipt.status == "accepted"
&& receipt.surface == "roleAccept"
&& receipt.role_id == coordinator_role_label(role_id)
})
}
fn role_finding_acceptance_receipt_index(
state: &EpiphanyThreadState,
role_id: EpiphanyCoordinatorRoleId,
finding: &EpiphanyRoleFindingInterpretation,
) -> Option<usize> {
let result_id = finding.runtime_result_id.clone()?;
state.acceptance_receipts.iter().position(|receipt| {
receipt.result_id == result_id
&& receipt.status == "accepted"
&& receipt.surface == "roleAccept"
&& receipt.role_id == coordinator_role_label(role_id)
})
}
fn coordinator_role_label(role_id: EpiphanyCoordinatorRoleId) -> &'static str {
match role_id {
EpiphanyCoordinatorRoleId::Implementation => "implementation",
EpiphanyCoordinatorRoleId::Imagination => "imagination",
EpiphanyCoordinatorRoleId::Modeling => "modeling",
EpiphanyCoordinatorRoleId::Verification => "verification",
EpiphanyCoordinatorRoleId::Reorientation => "reorientation",
}
}
fn verification_finding_allows_implementation(finding: &EpiphanyRoleFindingInterpretation) -> bool {
finding
.verdict
.as_deref()
.is_some_and(|verdict| verdict.eq_ignore_ascii_case("pass"))
}
fn verification_finding_needs_evidence(finding: &EpiphanyRoleFindingInterpretation) -> bool {
finding
.verdict
.as_deref()
.is_some_and(|verdict| verdict.eq_ignore_ascii_case("needs-evidence"))
}
fn coordinator_role_lane_from_role_board(
lane: &EpiphanyRoleBoardLane,
) -> EpiphanyCoordinatorRoleLane {
EpiphanyCoordinatorRoleLane {
id: lane.id,
status: lane.status,
}
}
pub fn recommend_coordinator_action(
input: EpiphanyCoordinatorInput,
) -> EpiphanyCoordinatorDecision {

View file

@ -1,16 +1,22 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyCrrcStateStatus {
Missing,
Ready,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyCrrcReorientAction {
Resume,
Regather,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyCrrcResultStatus {
MissingState,
MissingBinding,
@ -23,7 +29,8 @@ pub enum EpiphanyCrrcResultStatus {
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyCrrcAction {
Continue,
PrepareCheckpoint,
@ -34,7 +41,8 @@ pub enum EpiphanyCrrcAction {
RegatherManually,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyCrrcSceneAction {
Update,
Reorient,
@ -43,7 +51,8 @@ pub enum EpiphanyCrrcSceneAction {
ReorientAccept,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyCrrcRecommendation {
pub action: EpiphanyCrrcAction,
pub recommended_scene_action: Option<EpiphanyCrrcSceneAction>,

View file

@ -1,11 +1,14 @@
use epiphany_state_model::EpiphanyRetrievalState;
use epiphany_state_model::EpiphanyRetrievalStatus;
use epiphany_state_model::EpiphanyThreadState;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyRetrievalFreshnessStatus {
Missing,
Ready,
@ -14,51 +17,69 @@ pub enum EpiphanyRetrievalFreshnessStatus {
Unavailable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyRetrievalFreshness {
pub status: EpiphanyRetrievalFreshnessStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub semantic_available: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_indexed_at_unix_seconds: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub indexed_file_count: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub indexed_chunk_count: Option<u32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dirty_paths: Vec<PathBuf>,
pub note: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyGraphFreshnessStatus {
Missing,
Ready,
Stale,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyGraphFreshness {
pub status: EpiphanyGraphFreshnessStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub graph_freshness: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub checkpoint_id: Option<String>,
pub dirty_path_count: u32,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dirty_paths: Vec<PathBuf>,
pub open_question_count: u32,
pub open_gap_count: u32,
pub note: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyInvalidationStatus {
Unavailable,
Clean,
Changed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyInvalidationInput {
pub status: EpiphanyInvalidationStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub watched_root: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub observed_at_unix_seconds: Option<i64>,
pub changed_path_count: u32,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub changed_paths: Vec<PathBuf>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub graph_node_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub active_frontier_node_ids: Vec<String>,
pub note: String,
}
@ -78,8 +99,10 @@ pub struct EpiphanyFreshnessInput<'a> {
pub watcher: Option<EpiphanyFreshnessWatcherInput<'a>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyFreshnessView {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state_revision: Option<u64>,
pub retrieval: EpiphanyRetrievalFreshness,
pub graph: EpiphanyGraphFreshness,

View file

@ -4,8 +4,11 @@ use epiphany_state_model::EpiphanyRetrievalState;
use epiphany_state_model::EpiphanyRetrievalStatus;
use epiphany_state_model::EpiphanyRuntimeLink;
use epiphany_state_model::EpiphanyThreadState;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyJobStatus {
Idle,
Needed,
@ -18,22 +21,33 @@ pub enum EpiphanyJobStatus {
Unavailable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyJobView {
pub id: String,
pub kind: EpiphanyJobKind,
pub scope: String,
pub owner_role: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authority_scope: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub runtime_job_id: Option<String>,
pub status: EpiphanyJobStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub items_processed: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub items_total: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub progress_note: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_checkpoint_at_unix_seconds: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blocking_reason: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub active_thread_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub linked_subgoal_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub linked_graph_node_ids: Vec<String>,
}

View file

@ -10,12 +10,14 @@ mod role_board;
mod role_result;
mod scene;
mod state_patch;
mod view;
mod worker_launch;
pub use coordinator::EpiphanyCoordinatorAction;
pub use coordinator::EpiphanyCoordinatorAutomationAction;
pub use coordinator::EpiphanyCoordinatorCrrcRecommendation;
pub use coordinator::EpiphanyCoordinatorDecision;
pub use coordinator::EpiphanyCoordinatorFindingSignals;
pub use coordinator::EpiphanyCoordinatorInput;
pub use coordinator::EpiphanyCoordinatorRoleId;
pub use coordinator::EpiphanyCoordinatorRoleLane;
@ -23,9 +25,15 @@ pub use coordinator::EpiphanyCoordinatorRoleResultStatus;
pub use coordinator::EpiphanyCoordinatorRoleStatus;
pub use coordinator::EpiphanyCoordinatorSceneAction;
pub use coordinator::EpiphanyCoordinatorSignals;
pub use coordinator::EpiphanyCoordinatorSourceSignals;
pub use coordinator::EpiphanyCoordinatorStatus;
pub use coordinator::EpiphanyCoordinatorStatusInput;
pub use coordinator::coordinator_automation_action;
pub use coordinator::crrc_scene_action_to_coordinator_scene_action;
pub use coordinator::derive_coordinator_finding_signals;
pub use coordinator::derive_coordinator_status;
pub use coordinator::recommend_coordinator_action;
pub use coordinator::reorient_finding_already_accepted;
pub use coordinator::select_coordinator_automation_action;
pub use crrc::EpiphanyCrrcAction;
pub use crrc::EpiphanyCrrcInput;
@ -124,8 +132,16 @@ pub use scene::EpiphanySceneStatusCount;
pub use scene::EpiphanySceneSubgoal;
pub use scene::derive_scene;
pub use state_patch::EpiphanyRoleStatePatchDocument;
pub use view::EpiphanyViewLens;
pub use view::default_epiphany_view_lenses;
pub use view::epiphany_view_needs_jobs;
pub use view::epiphany_view_needs_pressure;
pub use view::epiphany_view_needs_reorientation_inputs;
pub use view::epiphany_view_needs_runtime_store;
pub use worker_launch::EpiphanyReorientLaunchRequestInput;
pub use worker_launch::EpiphanyReorientWorkerLaunchDocument;
pub use worker_launch::EpiphanyRoleWorkerLaunchDocument;
pub use worker_launch::EpiphanyWorkerLaunchDocument;
pub use worker_launch::REORIENT_WORKER_OUTPUT_CONTRACT_ID;
pub use worker_launch::ROLE_WORKER_OUTPUT_CONTRACT_ID;
pub use worker_launch::build_reorient_job_launch_request;

View file

@ -2,6 +2,9 @@ const EPIPHANY_PRESSURE_ELEVATED_PER_MILLE: u32 = 650;
const EPIPHANY_PRESSURE_PREPARE_COMPACTION_PER_MILLE: u32 = 800;
const EPIPHANY_PRESSURE_CRITICAL_PER_MILLE: u32 = 950;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EpiphanyTokenUsageSnapshot {
pub total_tokens: i64,
@ -10,27 +13,35 @@ pub struct EpiphanyTokenUsageSnapshot {
pub model_auto_compact_token_limit: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyPressure {
pub status: EpiphanyPressureStatus,
pub level: EpiphanyPressureLevel,
pub basis: EpiphanyPressureBasis,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub used_tokens: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_context_window: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_auto_compact_token_limit: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub remaining_tokens: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ratio_per_mille: Option<u32>,
pub should_prepare_compaction: bool,
pub note: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyPressureStatus {
Unknown,
Ready,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyPressureLevel {
Unknown,
Low,
@ -39,7 +50,8 @@ pub enum EpiphanyPressureLevel {
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyPressureBasis {
Unknown,
AutoCompactLimit,

View file

@ -1,29 +1,35 @@
use epiphany_state_model::EpiphanyInvestigationCheckpoint;
use epiphany_state_model::EpiphanyInvestigationDisposition;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyReorientAction {
Resume,
Regather,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyReorientStateStatus {
Missing,
Ready,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyReorientCheckpointStatus {
Missing,
ResumeReady,
RegatherRequired,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyReorientReason {
MissingState,
MissingCheckpoint,
@ -35,7 +41,8 @@ pub enum EpiphanyReorientReason {
UnanchoredCheckpointWhileStateStale,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyReorientPressureLevel {
Unknown,
Low,
@ -44,7 +51,8 @@ pub enum EpiphanyReorientPressureLevel {
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyReorientFreshnessStatus {
Unknown,
Clean,
@ -69,18 +77,24 @@ pub struct EpiphanyReorientInput<'a> {
pub watched_root: Option<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyReorientDecision {
pub action: EpiphanyReorientAction,
pub checkpoint_status: EpiphanyReorientCheckpointStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub checkpoint_id: Option<String>,
pub pressure_level: EpiphanyReorientPressureLevel,
pub retrieval_status: EpiphanyReorientFreshnessStatus,
pub graph_status: EpiphanyReorientFreshnessStatus,
pub watcher_status: EpiphanyReorientFreshnessStatus,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub reasons: Vec<EpiphanyReorientReason>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub checkpoint_dirty_paths: Vec<PathBuf>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub checkpoint_changed_paths: Vec<PathBuf>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub active_frontier_node_ids: Vec<String>,
pub next_action: String,
pub note: String,

View file

@ -3,8 +3,10 @@ use super::EpiphanyCoordinatorRoleStatus;
use super::EpiphanyCoordinatorSceneAction;
use super::EpiphanyCrrcAction;
use super::EpiphanyCrrcResultStatus;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyRoleBoardJobStatus {
Idle,
Needed,
@ -17,7 +19,8 @@ pub enum EpiphanyRoleBoardJobStatus {
Unavailable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyRoleBoardJob {
pub id: String,
pub owner_role: String,
@ -26,7 +29,8 @@ pub struct EpiphanyRoleBoardJob {
pub blocking_reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyRoleBoardPlanningSummary {
pub capture_count: usize,
pub backlog_item_count: usize,
@ -34,13 +38,15 @@ pub struct EpiphanyRoleBoardPlanningSummary {
pub objective_draft_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyRoleBoardCheckpointSummary {
pub disposition: Option<String>,
pub next_action: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyRoleBoardInput {
pub state_present: bool,
pub planning: EpiphanyRoleBoardPlanningSummary,
@ -61,7 +67,8 @@ pub struct EpiphanyRoleBoardInput {
pub imagination_owner_role: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyRoleBoardLane {
pub id: EpiphanyCoordinatorRoleId,
pub title: String,

View file

@ -1,4 +1,7 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyRoleResultRoleId {
Implementation,
Imagination,
@ -7,14 +10,16 @@ pub enum EpiphanyRoleResultRoleId {
Reorientation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyRoleSelfPersistenceStatus {
Missing,
Accepted,
Rejected,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyRoleSelfPersistenceReview {
pub status: EpiphanyRoleSelfPersistenceStatus,
pub target_agent_id: Option<String>,
@ -22,7 +27,8 @@ pub struct EpiphanyRoleSelfPersistenceReview {
pub reasons: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyRoleFindingInterpretation {
pub verdict: Option<String>,
pub summary: Option<String>,
@ -45,7 +51,8 @@ pub struct EpiphanyRoleFindingInterpretation {
pub item_error: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyRoleAcceptanceFinding {
pub role_id: EpiphanyRoleResultRoleId,
pub verdict: Option<String>,
@ -57,7 +64,8 @@ pub struct EpiphanyRoleAcceptanceFinding {
pub projected_fields: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyReorientAcceptanceFinding {
pub mode: Option<String>,
pub summary: Option<String>,
@ -68,7 +76,8 @@ pub struct EpiphanyReorientAcceptanceFinding {
pub runtime_job_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyReorientFindingInterpretation {
pub mode: Option<String>,
pub summary: Option<String>,
@ -84,7 +93,8 @@ pub struct EpiphanyReorientFindingInterpretation {
pub item_error: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyAcceptanceBundle {
pub accepted_receipt_id: String,
pub accepted_observation_id: String,
@ -94,7 +104,8 @@ pub struct EpiphanyAcceptanceBundle {
pub receipt: EpiphanyAcceptanceReceipt,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EpiphanyReorientAcceptanceBundle {
pub accepted_receipt_id: String,
pub accepted_observation_id: String,

View file

@ -0,0 +1,52 @@
use serde::Deserialize;
use serde::Serialize;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyViewLens {
Scene,
Jobs,
Roles,
Planning,
Pressure,
Reorient,
Crrc,
Coordinator,
}
pub fn default_epiphany_view_lenses() -> Vec<EpiphanyViewLens> {
vec![
EpiphanyViewLens::Scene,
EpiphanyViewLens::Jobs,
EpiphanyViewLens::Roles,
EpiphanyViewLens::Planning,
EpiphanyViewLens::Pressure,
EpiphanyViewLens::Reorient,
EpiphanyViewLens::Crrc,
EpiphanyViewLens::Coordinator,
]
}
pub fn epiphany_view_needs_jobs(lenses: &[EpiphanyViewLens]) -> bool {
lenses.contains(&EpiphanyViewLens::Jobs)
|| lenses.contains(&EpiphanyViewLens::Roles)
|| lenses.contains(&EpiphanyViewLens::Crrc)
|| lenses.contains(&EpiphanyViewLens::Coordinator)
}
pub fn epiphany_view_needs_reorientation_inputs(lenses: &[EpiphanyViewLens]) -> bool {
lenses.contains(&EpiphanyViewLens::Roles)
|| lenses.contains(&EpiphanyViewLens::Reorient)
|| lenses.contains(&EpiphanyViewLens::Crrc)
|| lenses.contains(&EpiphanyViewLens::Coordinator)
}
pub fn epiphany_view_needs_pressure(lenses: &[EpiphanyViewLens]) -> bool {
lenses.contains(&EpiphanyViewLens::Pressure) || epiphany_view_needs_reorientation_inputs(lenses)
}
pub fn epiphany_view_needs_runtime_store(lenses: &[EpiphanyViewLens]) -> bool {
lenses.contains(&EpiphanyViewLens::Roles)
|| lenses.contains(&EpiphanyViewLens::Crrc)
|| lenses.contains(&EpiphanyViewLens::Coordinator)
}

View file

@ -6,12 +6,23 @@ use epiphany_state_model::EpiphanyGraphFrontier;
use epiphany_state_model::EpiphanyGraphs;
use epiphany_state_model::EpiphanyInvariant;
use epiphany_state_model::EpiphanyInvestigationCheckpoint;
use epiphany_state_model::EpiphanyInvestigationDisposition;
use epiphany_state_model::EpiphanyJobKind;
use epiphany_state_model::EpiphanyObservation;
use epiphany_state_model::EpiphanyPlanningState;
use epiphany_state_model::EpiphanyScratchPad;
use epiphany_state_model::EpiphanySubgoal;
use epiphany_state_model::EpiphanyThreadState;
use serde::Deserialize;
use serde::Serialize;
use std::path::Path;
use crate::EpiphanyJobLaunchRequest;
use crate::EpiphanyReorientAction;
use crate::EpiphanyReorientDecision;
use crate::EpiphanyReorientFreshnessStatus;
use crate::EpiphanyReorientPressureLevel;
use crate::EpiphanyReorientReason;
pub const ROLE_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.role_result.v0";
pub const REORIENT_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.reorient_result.v0";
@ -120,3 +131,285 @@ pub struct EpiphanyReorientWorkerLaunchDocument {
#[serde(default)]
pub linked_graph_node_ids: Vec<String>,
}
pub struct EpiphanyReorientLaunchRequestInput<'a> {
pub thread_id: &'a str,
pub expected_revision: Option<u64>,
pub max_runtime_seconds: Option<u64>,
pub binding_id: &'a str,
pub owner_role: &'a str,
pub instruction: String,
pub state: &'a EpiphanyThreadState,
pub checkpoint: &'a EpiphanyInvestigationCheckpoint,
pub decision: &'a EpiphanyReorientDecision,
}
pub fn build_reorient_job_launch_request(
input: EpiphanyReorientLaunchRequestInput<'_>,
) -> EpiphanyJobLaunchRequest {
let linked_subgoal_ids = active_subgoal_ids(input.state);
let linked_graph_node_ids = unique_strings(
active_graph_node_ids(input.state)
.into_iter()
.chain(input.decision.active_frontier_node_ids.iter().cloned()),
);
let launch_document =
EpiphanyWorkerLaunchDocument::Reorient(build_reorient_worker_launch_document(
input.thread_id,
input.state,
input.checkpoint,
input.decision,
linked_subgoal_ids.clone(),
linked_graph_node_ids.clone(),
));
let output_contract_id = launch_document.output_contract_id().to_string();
EpiphanyJobLaunchRequest {
expected_revision: input.expected_revision,
binding_id: input.binding_id.to_string(),
kind: EpiphanyJobKind::Specialist,
scope: reorient_launch_scope(input.decision.action).to_string(),
owner_role: input.owner_role.to_string(),
authority_scope: reorient_launch_authority_scope(input.decision.action).to_string(),
linked_subgoal_ids,
linked_graph_node_ids,
instruction: input.instruction,
launch_document,
output_contract_id,
max_runtime_seconds: input.max_runtime_seconds,
}
}
fn build_reorient_worker_launch_document(
thread_id: &str,
state: &EpiphanyThreadState,
checkpoint: &EpiphanyInvestigationCheckpoint,
decision: &EpiphanyReorientDecision,
linked_subgoal_ids: Vec<String>,
linked_graph_node_ids: Vec<String>,
) -> EpiphanyReorientWorkerLaunchDocument {
EpiphanyReorientWorkerLaunchDocument {
thread_id: thread_id.to_string(),
mode: reorient_action_label(decision.action).to_string(),
checkpoint_id: checkpoint.checkpoint_id.clone(),
checkpoint_kind: checkpoint.kind.clone(),
checkpoint_disposition: investigation_disposition_label(checkpoint.disposition).to_string(),
checkpoint_focus: Some(checkpoint.focus.clone()),
checkpoint_summary: checkpoint.summary.clone(),
checkpoint_next_action: checkpoint
.next_action
.clone()
.unwrap_or_else(|| decision.next_action.clone()),
checkpoint_open_questions: checkpoint.open_questions.clone(),
checkpoint_evidence_ids: checkpoint.evidence_ids.clone(),
checkpoint_code_refs: checkpoint.code_refs.clone(),
decision_reasons: decision
.reasons
.iter()
.map(|reason| reorient_reason_label(*reason).to_string())
.collect(),
decision_note: decision.note.clone(),
pressure_level: reorient_pressure_level_label(decision.pressure_level).to_string(),
retrieval_status: reorient_freshness_status_label(decision.retrieval_status).to_string(),
graph_status: reorient_freshness_status_label(decision.graph_status).to_string(),
watcher_status: reorient_freshness_status_label(decision.watcher_status).to_string(),
checkpoint_dirty_paths: decision
.checkpoint_dirty_paths
.iter()
.map(path_to_display_string)
.collect(),
checkpoint_changed_paths: decision
.checkpoint_changed_paths
.iter()
.map(path_to_display_string)
.collect(),
scratch: state.scratch.clone(),
graphs: Some(state.graphs.clone()),
recent_evidence: state.recent_evidence.iter().take(8).cloned().collect(),
recent_observations: state.observations.iter().take(8).cloned().collect(),
active_frontier_node_ids: decision.active_frontier_node_ids.clone(),
linked_subgoal_ids,
linked_graph_node_ids,
}
}
fn active_subgoal_ids(state: &EpiphanyThreadState) -> Vec<String> {
state
.active_subgoal_id
.clone()
.map(|id| vec![id])
.unwrap_or_default()
}
fn active_graph_node_ids(state: &EpiphanyThreadState) -> Vec<String> {
state
.graph_frontier
.as_ref()
.map(|frontier| frontier.active_node_ids.clone())
.unwrap_or_default()
}
fn unique_strings(values: impl IntoIterator<Item = String>) -> Vec<String> {
let mut unique = Vec::new();
for value in values {
if !unique.iter().any(|existing| existing == &value) {
unique.push(value);
}
}
unique
}
fn path_to_display_string(path: impl AsRef<Path>) -> String {
path.as_ref().to_string_lossy().to_string()
}
fn reorient_launch_authority_scope(action: EpiphanyReorientAction) -> &'static str {
match action {
EpiphanyReorientAction::Resume => "epiphany.reorient.resume",
EpiphanyReorientAction::Regather => "epiphany.reorient.regather",
}
}
fn reorient_launch_scope(action: EpiphanyReorientAction) -> &'static str {
match action {
EpiphanyReorientAction::Resume => "reorient-guided checkpoint resume",
EpiphanyReorientAction::Regather => "reorient-guided checkpoint regather",
}
}
fn reorient_action_label(action: EpiphanyReorientAction) -> &'static str {
match action {
EpiphanyReorientAction::Resume => "resume",
EpiphanyReorientAction::Regather => "regather",
}
}
fn investigation_disposition_label(disposition: EpiphanyInvestigationDisposition) -> &'static str {
match disposition {
EpiphanyInvestigationDisposition::ResumeReady => "resume_ready",
EpiphanyInvestigationDisposition::RegatherRequired => "regather_required",
}
}
fn reorient_reason_label(reason: EpiphanyReorientReason) -> &'static str {
match reason {
EpiphanyReorientReason::MissingState => "missingState",
EpiphanyReorientReason::MissingCheckpoint => "missingCheckpoint",
EpiphanyReorientReason::CheckpointRequestedRegather => "checkpointRequestedRegather",
EpiphanyReorientReason::CheckpointPathsDirty => "checkpointPathsDirty",
EpiphanyReorientReason::CheckpointPathsChanged => "checkpointPathsChanged",
EpiphanyReorientReason::FrontierChanged => "frontierChanged",
EpiphanyReorientReason::UnanchoredCheckpointWhileStateStale => {
"unanchoredCheckpointWhileStateStale"
}
EpiphanyReorientReason::CheckpointReady => "checkpointReady",
}
}
fn reorient_pressure_level_label(level: EpiphanyReorientPressureLevel) -> &'static str {
match level {
EpiphanyReorientPressureLevel::Unknown => "unknown",
EpiphanyReorientPressureLevel::Low => "low",
EpiphanyReorientPressureLevel::Medium => "medium",
EpiphanyReorientPressureLevel::High => "high",
EpiphanyReorientPressureLevel::Critical => "critical",
}
}
fn reorient_freshness_status_label(status: EpiphanyReorientFreshnessStatus) -> &'static str {
match status {
EpiphanyReorientFreshnessStatus::Unknown => "unknown",
EpiphanyReorientFreshnessStatus::Clean => "clean",
EpiphanyReorientFreshnessStatus::Dirty => "dirty",
EpiphanyReorientFreshnessStatus::Stale => "stale",
EpiphanyReorientFreshnessStatus::Changed => "changed",
}
}
#[cfg(test)]
mod tests {
use super::*;
use epiphany_state_model::EpiphanyCodeRef;
use epiphany_state_model::EpiphanyGraphFrontier;
use std::path::PathBuf;
#[test]
fn reorient_launch_request_is_composed_from_typed_state() {
let state = EpiphanyThreadState {
revision: 7,
active_subgoal_id: Some("sg-1".to_string()),
graph_frontier: Some(EpiphanyGraphFrontier {
active_node_ids: vec!["node-a".to_string()],
active_edge_ids: Vec::new(),
open_question_ids: Vec::new(),
open_gap_ids: Vec::new(),
dirty_paths: Vec::new(),
}),
..Default::default()
};
let checkpoint = EpiphanyInvestigationCheckpoint {
checkpoint_id: "ix-1".to_string(),
kind: "implementation".to_string(),
disposition: EpiphanyInvestigationDisposition::RegatherRequired,
focus: "runtime seam".to_string(),
summary: Some("resume here".to_string()),
next_action: None,
captured_at_turn_id: Some("turn-1".to_string()),
open_questions: Vec::new(),
code_refs: vec![EpiphanyCodeRef {
path: PathBuf::from("src/lib.rs"),
start_line: None,
end_line: None,
symbol: None,
note: None,
}],
evidence_ids: vec!["ev-1".to_string()],
};
let decision = EpiphanyReorientDecision {
action: EpiphanyReorientAction::Regather,
checkpoint_status: crate::EpiphanyReorientCheckpointStatus::RegatherRequired,
checkpoint_id: Some("ix-1".to_string()),
pressure_level: EpiphanyReorientPressureLevel::High,
retrieval_status: EpiphanyReorientFreshnessStatus::Stale,
graph_status: EpiphanyReorientFreshnessStatus::Changed,
watcher_status: EpiphanyReorientFreshnessStatus::Dirty,
reasons: vec![EpiphanyReorientReason::FrontierChanged],
checkpoint_dirty_paths: vec![PathBuf::from("src/lib.rs")],
checkpoint_changed_paths: vec![PathBuf::from("src/main.rs")],
active_frontier_node_ids: vec!["node-b".to_string()],
next_action: "Regather the seam.".to_string(),
note: "continuity moved".to_string(),
};
let request = build_reorient_job_launch_request(EpiphanyReorientLaunchRequestInput {
thread_id: "thread-1",
expected_revision: Some(state.revision),
max_runtime_seconds: Some(60),
binding_id: "reorient-worker",
owner_role: "epiphany-reorient",
instruction: "do the reorientation".to_string(),
state: &state,
checkpoint: &checkpoint,
decision: &decision,
});
assert_eq!(request.binding_id, "reorient-worker");
assert_eq!(request.scope, "reorient-guided checkpoint regather");
assert_eq!(request.authority_scope, "epiphany.reorient.regather");
assert_eq!(request.linked_subgoal_ids, vec!["sg-1"]);
assert_eq!(
request.linked_graph_node_ids,
vec!["node-a".to_string(), "node-b".to_string()]
);
let EpiphanyWorkerLaunchDocument::Reorient(document) = request.launch_document else {
panic!("expected reorient launch document");
};
assert_eq!(document.mode, "regather");
assert_eq!(document.checkpoint_next_action, "Regather the seam.");
assert_eq!(document.decision_reasons, vec!["frontierChanged"]);
assert_eq!(document.pressure_level, "high");
assert_eq!(document.retrieval_status, "stale");
assert_eq!(document.graph_status, "changed");
assert_eq!(document.watcher_status, "dirty");
}
}

View file

@ -20,9 +20,7 @@ fn epiphany_agent_memory_can_move_over_cultnet_documents() -> Result<()> {
let mut registry = CultNetDocumentRegistry::new();
registry.register(
CultNetDocumentBinding::for_entry::<EpiphanyAgentMemoryEntry>(
"ghostlight.agent_state.v0",
),
CultNetDocumentBinding::for_entry::<EpiphanyAgentMemoryEntry>("ghostlight.agent_state.v0"),
);
let snapshot = registry.create_snapshot_response(
&source_cache,

View file

@ -10,5 +10,5 @@ name = "epiphany_openai_adapter"
path = "src/lib.rs"
[dependencies]
cultcache-rs = { path = "../vendor/cultcache-rs" }
cultcache-rs = { path = "../../CultLib/crates/cultcache-rs" }
serde = { version = "1", features = ["derive"] }

View file

@ -5,6 +5,10 @@ use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
mod prompt;
pub use prompt::render_epiphany_state;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
pub struct EpiphanyStateItem {
#[serde(default, skip_serializing_if = "Option::is_none")]

View file

@ -1,19 +1,19 @@
use epiphany_state_model::EpiphanyChurnState;
use epiphany_state_model::EpiphanyCodeRef;
use epiphany_state_model::EpiphanyEvidenceRecord;
use epiphany_state_model::EpiphanyGraph;
use epiphany_state_model::EpiphanyGraphCheckpoint;
use epiphany_state_model::EpiphanyGraphEdge;
use epiphany_state_model::EpiphanyGraphFrontier;
use epiphany_state_model::EpiphanyGraphLink;
use epiphany_state_model::EpiphanyGraphNode;
use epiphany_state_model::EpiphanyInvestigationCheckpoint;
use epiphany_state_model::EpiphanyInvestigationDisposition;
use epiphany_state_model::EpiphanyModeState;
use epiphany_state_model::EpiphanyObservation;
use epiphany_state_model::EpiphanyPlanningState;
use epiphany_state_model::EpiphanyScratchPad;
use epiphany_state_model::EpiphanyThreadState;
use crate::EpiphanyChurnState;
use crate::EpiphanyCodeRef;
use crate::EpiphanyEvidenceRecord;
use crate::EpiphanyGraph;
use crate::EpiphanyGraphCheckpoint;
use crate::EpiphanyGraphEdge;
use crate::EpiphanyGraphFrontier;
use crate::EpiphanyGraphLink;
use crate::EpiphanyGraphNode;
use crate::EpiphanyInvestigationCheckpoint;
use crate::EpiphanyInvestigationDisposition;
use crate::EpiphanyModeState;
use crate::EpiphanyObservation;
use crate::EpiphanyPlanningState;
use crate::EpiphanyScratchPad;
use crate::EpiphanyThreadState;
use std::collections::HashSet;
use std::path::Path;
@ -798,19 +798,19 @@ fn push_omitted_count(lines: &mut Vec<String>, total: usize, shown: usize, label
#[cfg(test)]
mod tests {
use super::render_epiphany_state;
use epiphany_state_model::EpiphanyChurnState;
use epiphany_state_model::EpiphanyCodeRef;
use epiphany_state_model::EpiphanyGraph;
use epiphany_state_model::EpiphanyGraphCheckpoint;
use epiphany_state_model::EpiphanyGraphEdge;
use epiphany_state_model::EpiphanyGraphFrontier;
use epiphany_state_model::EpiphanyGraphNode;
use epiphany_state_model::EpiphanyGraphs;
use epiphany_state_model::EpiphanyInvestigationCheckpoint;
use epiphany_state_model::EpiphanyInvestigationDisposition;
use epiphany_state_model::EpiphanyModeKind;
use epiphany_state_model::EpiphanyModeState;
use epiphany_state_model::EpiphanyThreadState;
use crate::EpiphanyChurnState;
use crate::EpiphanyCodeRef;
use crate::EpiphanyGraph;
use crate::EpiphanyGraphCheckpoint;
use crate::EpiphanyGraphEdge;
use crate::EpiphanyGraphFrontier;
use crate::EpiphanyGraphNode;
use crate::EpiphanyGraphs;
use crate::EpiphanyInvestigationCheckpoint;
use crate::EpiphanyInvestigationDisposition;
use crate::EpiphanyModeKind;
use crate::EpiphanyModeState;
use crate::EpiphanyThreadState;
use std::path::PathBuf;
#[test]

View file

@ -0,0 +1,53 @@
# Body CultNet Contracts
Objective: make Body the repository access guardian. The repo is substrate, not
a free-for-all evidence buffet. Organs may request reads, indexing, commands,
edits, and bridge operations, but Body decides the scoped access to the repo.
## Authority Map
- Owner: Body owns repository access.
- Inputs: typed repo access requests, workspace identity, requested paths,
requested operations, command/bridge intent, current repo policy, coordinator
authority, operator approvals, and relevant safety/evidence context.
- Outputs: Body access reviews, access grant receipts, access refusal receipts,
repo snapshot receipts, and repo mutation receipts.
- Derived state: retrieved snippets, search hits, index manifests, diffs, build
logs, Unity/Rider bridge artifacts, and source maps are evidence projections,
not permission to touch more substrate.
- Forbidden writers/readers: Hands, Eyes, Face, public Verse ingress, raw worker
launches, compatibility JSON-RPC routes, and bridge tools must not directly
read, index, execute against, or mutate the repo without a scoped Body grant.
- Shared path: source reads, semantic indexing, file edits, command execution,
and editor/runtime bridge operations should all be expressible as Body-gated
repo access requests with receipts.
- Deletion line: worker launch `authority_scope` is not repo access authority.
It may describe role/task scope, but Body must grant substrate access.
## Contract Families
- `epiphany.body.repo_access_request`: request for scoped repository access.
- `epiphany.body.repo_access_review`: Body's decision and scope explanation.
- `epiphany.body.repo_access_grant_receipt`: proof of scoped access.
- `epiphany.body.repo_access_refusal_receipt`: proof of refusal.
- `epiphany.body.repo_snapshot_receipt`: proof of source inspection/indexing
performed under Body access.
- `epiphany.body.repo_mutation_receipt`: proof that mutation or repo-affecting
command execution had a prior Body grant.
## Verse Boundary
Body access contracts live in `epiphany-internal`. Operator-safe projections may
appear in `gamecult-local` after review, but raw repo access requests, private
paths, command cargo, diffs, and mutation receipts stay internal unless another
explicit export policy says otherwise. `epiphany-global` never gains repository
access authority.
Mind and Body are neighboring gates, not aliases. Body decides whether the
machine may touch the repo. Mind decides whether resulting thought or evidence
mutates persistent state.
Imagination can project Body facts into a Face scene only after those facts have
entered through Body-gated access. A beautiful scene is not a repo access grant.
Eyes is the next gate after access: a Body grant allows looking, but only Eyes
can turn looked-at material into a citable evidence packet.

View file

@ -0,0 +1,618 @@
# Epiphany CultMesh Dreaming Roadmap
This document turns the CultMesh dreaming idea into an architecture target for
Epiphany.
The short version:
Epiphany instances should eventually speak to one another over CultMesh by
sharing typed public thought artifacts. They must not share private state by
accident, and they must not treat foreign dreams as authority. The dream layer
is pollen, not a remote-control cable.
## Objective
Build Epiphany as a distributed typed-state organism whose private mind remains
local, while selected public thoughts, questions, hypotheses, findings, and
receipts can travel between Epiphany instances through CultMesh.
The goal is not one merged global agent mind. That way lies mush with a network
port.
The goal is a mesh of local Epiphanies that can:
- publish selected dream artifacts intentionally
- subscribe to public dreams from trusted or public Verses
- cite, fork, answer, cool, quarantine, or adopt those dreams locally
- preserve provenance and export policy on every shared artifact
- keep private memory, operator context, credentials, and raw worker thought
sealed unless a separate reviewed export creates a public document
## Current Mechanism
Epiphany already has several organs pointing in this direction:
- CultCache-shaped typed state stores for thread state, ledgers, role dossiers,
heartbeat state, runtime-spine state, and memory graph documents.
- CultNet contract advertisement in runtime-spine hello documents.
- Heartbeat/routine physiology with incubation, dream residue, thought lanes,
bridge syntheses, candidate interventions, appraisals, and reactions.
- Face/public-surface packets that can turn role-local state into public speech
or bubble artifacts.
- Memory graph profile producers that treat repo graph, role memory,
short-term pressure, incubation, agency, and evidence as profiles of one
typed graph substrate.
CultMesh already supplies the transport anatomy this needs:
- CultCache owns typed documents, record keys, indexes, local persistence, and
local diffing.
- CultNet owns schema-v0 wire messages, transport, shard authority, remote
mutation delivery, subscription fanout, snapshot catch-up, and provenance.
- CultMesh owns the runtime-facing distributed database surface, Verse
discovery, peer exchange, authority leases, shard logs, replica catch-up, and
higher-level node entrypoints.
The missing organ is an Epiphany-owned export/import policy and typed document
family for public dreams.
## Ownership Map
### CultCache
CultCache owns the local typed document model.
It should store:
- private local Epiphany state
- local organ state
- public dream documents authored by this instance
- imported foreign dream documents after they pass ingress policy
- dream lineage indexes and local adoption notes
CultCache does not decide whether a private memory may be exported. It only
stores typed documents and makes their schema visible.
### Epiphany
Epiphany owns export policy, import policy, digestion, and local adoption.
It decides:
- which local state may become a public dream
- which fields must be omitted or summarized
- which Verse and shard a document belongs to
- whether a foreign dream is ignored, cooled, quarantined, cited, forked,
answered, or digested into local memory
- whether an imported dream can influence Face speech, memory graph context,
planning, or future objective drafts
Epiphany must never export private state by flipping a visibility flag on the
private document. Public dreams are separately authored documents.
### CultNet
CultNet owns the wire.
It moves raw typed documents, snapshots, shard logs, and subscriptions. It
preserves provenance fields such as source runtime, source agent, source role,
record key, schema id, tags, and stored time.
CultNet does not infer consent, privacy, or dream meaning.
### CultMesh
CultMesh owns the distributed runtime surface.
It provides:
- Verse discovery
- peer exchange
- shard ownership
- read replicas
- authority leases
- durable shard logs
- snapshot recovery
- watchable typed records
CultMesh can enforce which runtime may write to a shard. It does not by itself
know that a private wound note must never become a public dream.
### Heimdall / Identity Authority
Heimdall or its eventual Epiphany-native equivalent should own member identity,
OAuth, Discord/account linking, signing keys, and human/agent identity mapping.
Dream signatures should eventually depend on this authority surface. Until then,
development Verses may use local operator keys or unsigned local-only dreams.
### Bifrost / Governance Authority
Bifrost should consume dream-derived proposals when they become governance
material. It should not be the dream transport itself unless it is acting as a
CultMesh client over the same typed documents.
Dreams can seed discussions. Governance still needs explicit threads, votes,
consensus, bounty/reward policy, and accepted work items.
## State Sharing Levels
Epiphany needs explicit sharing classes. These are export classes, not vibes.
| Level | Name | Storage | Transport | Examples |
| --- | --- | --- | --- | --- |
| 0 | Private local state | Local CultCache only | None | credentials, raw worker thought, private operator context, unreviewed wounds |
| 1 | Local organ state | Local/private Verse | Local node only or trusted localhost mesh | heartbeat, role dossiers, Face state, runtime-spine jobs |
| 2 | Trusted swarm state | GameCult-controlled Verse | CultMesh with leases | turn leases, receipts, non-secret status, reviewable findings |
| 3 | Public dreaming surface | Public or semi-public Verse | CultMesh publish/subscribe | dreams, questions, hypotheses, design pressure, myth fragments |
| 4 | Public artifacts | Public web/Git/Discord/Bifrost mirrors | CultMesh plus external publication | posts, proposals, public receipts, accepted governance records |
The critical invariant:
Private state is not shared with a flag flipped off. Shared dreams are
separately authored public documents.
## Dream Document Family
The first document family should be narrow and boring enough to implement.
### `EpiphanyDream`
Purpose: a public thought artifact that may influence another instance but is
not adopted truth.
Suggested fields:
- `schema_version`
- `dream_id`
- `author_instance_id`
- `author_organ_id`
- `author_face_id`
- `source_repo`
- `source_topic`
- `visibility`: `public_dream`, `trusted_swarm`, or `local_only_export_preview`
- `title`
- `body`
- `dream_kind`
- `confidence`
- `stance`
- `tags`
- `lineage`
- `citations`
- `export_policy`
- `expires_at`
- `created_at`
- `signature`
`dream_kind` should start as a closed enum:
- `question`
- `hypothesis`
- `pattern_noticed`
- `design_pressure`
- `failed_path`
- `myth_fragment`
- `invitation`
- `receipt_summary`
### `EpiphanyDreamLineage`
Purpose: connect a dream to other dreams without pretending that connection is
consensus.
Suggested fields:
- `parent_dream_ids`
- `forked_from`
- `answers`
- `contradicts`
- `cites`
- `cooling_reason`
- `adoption_receipt_id`
### `EpiphanyDreamIngressReceipt`
Purpose: record how a foreign dream entered local Epiphany and what happened to
it.
Suggested fields:
- `receipt_id`
- `foreign_dream_id`
- `source_peer_id`
- `source_verse_id`
- `schema_status`
- `signature_status`
- `policy_decision`
- `local_record_key`
- `reason`
- `created_at`
Policy decisions:
- `store_only`
- `surface_to_face`
- `surface_to_eyes`
- `add_to_memory_graph_context`
- `fork_locally`
- `answer`
- `cool`
- `quarantine`
- `reject`
### `EpiphanyDreamAdoptionReceipt`
Purpose: record local digestion when a dream changes durable local state.
Suggested fields:
- `receipt_id`
- `dream_id`
- `adopted_by_instance_id`
- `adopted_by_organ_id`
- `target_surface`
- `summary`
- `local_state_refs`
- `verification_refs`
- `created_at`
Adoption is the hard boundary. A dream can be stored and discussed without
becoming memory, objective, doctrine, or project truth.
## Verse Design
Start with three Verses. This is an ownership split, not naming garnish.
### `epiphany-internal`
Authority model: one Epiphany instance or its trusted local host processes.
Purpose:
- sub-agent typed state
- heartbeat, role dossiers, runtime-spine jobs, private receipts, and local
organ state
- private subscriptions between Aquarium, CLI, Face, heartbeat,
runtime-spine, and local provider runners owned by the same Epiphany
This Verse may carry private state. It must not accept untrusted ingress, public
peers, Yggdrasil tunnels, or other GameCult app writes.
### `gamecult-local`
Authority model: GameCult-controlled local-area mesh with explicit leases.
Purpose:
- trusted sharing between local GameCult projects such as VoidBot, Epiphany,
Mimir, Aquarium, and bridge services
- non-secret node presence, provider capability, reviewable findings, receipts,
and operator-safe status
- optional tunnels to services running on Yggdrasil when explicitly configured
as GameCult-trusted peers
This Verse may cross machines on the local network and may bridge through a
tunnel to Yggdrasil. It must not carry raw worker thought, credentials, private
operator context, or unreviewed private memory.
### `epiphany-global`
Authority model: public/federated with hostile-ingress assumptions.
Purpose:
- untrusted public dreams
- public questions, hypotheses, design pressure, invitations, lineage, ingress
receipts, cooling/quarantine metadata, and adoption receipts
- Imagination's public dream exchange with other Epiphanies on the internet
- topic-specific threaded public chatrooms, roughly Reddit-shaped, where Faces
can post public thread roots and replies in the room that matches the subject
This Verse is public thought weather. Nothing from it mutates local memory,
planning, doctrine, governance, or project truth without a reviewed local
adoption receipt.
Initial global rooms should be varied but closed enough to moderate:
- `epiphany-global/dreams`: dreams, symbolic fragments, imaginative pressure,
and unfinished possible worlds
- `epiphany-global/architecture`: ownership maps, protocol boundaries, system
design, and rejected machine shapes
- `epiphany-global/research`: prior art, papers, source-grounded findings, and
scout reports
- `epiphany-global/faces`: Face identity, voice, public presence, and social
surface
- `epiphany-global/gamecult`: GameCult project coordination and cross-project
public questions
- `epiphany-global/governance`: public proposals before any Bifrost adoption
Faces may post there only through public-surface policy: no raw worker thought,
credentials, private operator context, private memory, or unreviewed internal
state. A global room post is a public artifact candidate or dream artifact, not
local truth.
Writes require an authority lease scoped to the target Verse and shard. Public
read replicas may subscribe to global dream streams after the schema and
identity policy exist.
Shard candidates:
- `dreams.by_instance`
- `dreams.by_topic`
- `dreams.by_repo`
- `dreams.lineage`
- `dreams.receipts`
For the first global slice, use one primary shard such as `dreams.public` and
resist premature shard theater. Internal and local-area shards should be named
by the document family they actually own, not by the dream system.
## Export Policy
Export is a separate act from persistence.
Allowed export sources:
- reviewed Face public thoughts
- reviewed heartbeat dream residue
- reviewed memory graph context cuts
- reviewable findings already safe for public discussion
- explicit operator-authored public notes
Forbidden export sources:
- raw worker transcripts
- raw model reasoning
- credentials
- tokens
- private operator context
- direct messages unless explicitly exported by policy
- private memory notes
- unreviewed selfPatch payloads
- sealed forensic artifacts
Export checks:
1. The source document has a typed schema.
2. The export class permits public dreaming.
3. The export projector creates a new public dream document.
4. The dream document contains no private source fields.
5. The dream carries provenance, lineage, confidence, and export policy.
6. The write target Verse/shard is valid.
7. The local instance holds a lease or client authority scope for that write.
8. The export writes an auditable receipt.
If any check fails, no document is published.
## Import Policy
Foreign dreams are external thought weather.
They should enter through an ingress gate that:
1. validates schema
2. validates signature when signatures exist
3. checks Verse and peer trust
4. checks saturation and motif repetition
5. stores or rejects the dream
6. emits an ingress receipt
7. routes the dream to local organs only through typed context cuts
Imported dreams must not mutate:
- role dossiers
- project truth
- objectives
- active plans
- governance state
- repo files
unless a separate local adoption receipt is reviewed and accepted.
## Anti-Infection Physiology
VoidBot already demonstrated the failure mode: repeated attractive language can
turn into a little religion before anyone notices.
Dream sharing needs immune surfaces:
- motif saturation per instance, topic, source peer, and Verse
- refractory cooling for repeated motifs
- lineage visibility so repeated ideas show their ancestry
- contradiction preservation instead of consensus flattening
- quarantine for dreams that are too private, too repetitive, malformed, or
source-uncertain
- explicit distinction between `dream`, `hypothesis`, `proposal`, `accepted`,
and `doctrine`
- local adoption receipts for any durable memory mutation
The dream layer should make ideas travel. It should not make every instance
recite the same sentence with a different avatar.
## Product Shape
Aquarium should become the operator window into dream flow:
- local private state status, without exposing content
- local dream export preview
- public dream stream
- ingress receipts
- saturation/cooling state
- lineage graph
- adoption queue
- rejected/quarantined dreams
- Face-readable dream summaries
Discord should receive only selected public mirrors. It should not be the source
of truth for dream state.
Bifrost should receive only dreams that have become governance material or
public proposals.
## Roadmap
### Phase 0: Contract Decision
Output:
- this design document
- map and implementation-plan pointers
- no runtime behavior change
Acceptance:
- future agents can name the ownership split without rereading Discord chat
- private-state/export distinction is explicit
### Phase 1: Dream Schema
Build typed Rust documents for:
- `EpiphanyDream`
- `EpiphanyDreamLineage`
- `EpiphanyDreamIngressReceipt`
- `EpiphanyDreamAdoptionReceipt`
Likely home:
- `epiphany-state-model` for durable schema types
- `epiphany-core` for validation and policy
Acceptance:
- schema validates closed enums and required provenance
- private/export class is typed, not a free string
- unit tests reject malformed dreams and forbidden visibility transitions
### Phase 2: Local Dream Store
Add a local CultCache-backed dream store CLI.
Commands:
- `status`
- `put-local-preview`
- `validate`
- `list`
- `ingress-receipt`
- `adoption-receipt`
Acceptance:
- no CultNet publishing yet
- a local dream preview can be written and read back
- receipts are stored as typed documents
### Phase 3: Export Projector
Build a policy function that turns safe local sources into new public dream
documents.
Sources:
- Face bubble artifacts
- reviewed heartbeat dream residue
- memory graph context cuts
- explicit operator notes
Acceptance:
- export creates a new document, never mutates private state in place
- forbidden source classes fail loudly
- export receipt records why the dream was allowed
### Phase 4: CultMesh Internal Verse
Start `epiphany-internal` over CultMesh with sub-agent state and local preview
documents registered in the document registry.
Acceptance:
- local node can watch dream records
- Aquarium or CLI can subscribe to changes
- shard writes obey the local primary
- no remote public sharing yet
### Phase 5: GameCult Local-Area Verse Prototype
Introduce `gamecult-local` as a trusted LAN/tunnel Verse.
Acceptance:
- two local GameCult projects can exchange non-secret status documents
- Yggdrasil tunnel peers require explicit peer cards and leases
- private internal Verse documents are rejected at the boundary
- provenance survives the round trip
### Phase 6: Public Dream Verse Prototype
Introduce `epiphany-global` as a development public Verse.
Acceptance:
- two local nodes can exchange public dream documents
- global room policies define topic-specific threaded posting surfaces for Faces
- authority lease or client scope is required for writes
- read replica can catch up through snapshot/log path
- provenance survives the round trip
### Phase 7: Import Digestion
Implement foreign dream ingress policy and local digestion queues.
Acceptance:
- foreign dream can be stored without becoming memory
- policy can cool, quarantine, reject, surface, fork, or answer
- adoption requires a separate reviewed receipt
- memory graph context can include imported dreams as external thought weather
### Phase 8: Face And Heartbeat Integration
Integrate dream flow into Epiphany's existing physiology.
Acceptance:
- Face can notice public dream weather without mistaking it for local memory
- heartbeat can publish reviewed dream residue
- saturation/refractory terms account for foreign motifs
- public speech can cite dream lineage when relevant
### Phase 9: Governance And Publication Bridges
Bridge mature dreams into Bifrost and public mirrors.
Acceptance:
- a dream can seed a Bifrost governance thread
- governance state remains separate from dream state
- Discord mirrors include links/provenance instead of becoming authority
- public artifacts can cite dream/adoption receipts
## Non-Goals
- Do not share private state.
- Do not create one merged Epiphany mind.
- Do not let public dreams mutate local memory without adoption.
- Do not make Discord the dream transport.
- Do not let Bifrost own dream transport just because governance may consume
dream-derived proposals.
- Do not invent a parallel transport when CultMesh already supplies typed
document replication, Verse discovery, shard authority, and subscription
fanout.
- Do not use JSON blobs as the internal dream payload when both sides are ours.
## First Implementation Slice
The first code slice should be Phase 1 plus a small Phase 2 store:
1. Add dream document types to `epiphany-state-model`.
2. Add validation policy to `epiphany-core`.
3. Add a local `epiphany-dream-store` binary that can status/validate/put/list
against a CultCache-backed store.
4. Add tests proving private/export class boundaries and lineage validation.
Only after that should CultMesh node wiring begin.
The machine should learn to label the dream before it teaches the dream to
travel.

View file

@ -14,13 +14,13 @@ The spine exists in more than twenty-five live paths:
- protocol state shape: `EpiphanyThreadState`, `RolloutItem::EpiphanyState`, retrieval summaries, and prompt tags live in [protocol.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/protocol/src/protocol.rs:103).
- in-memory session state: `SessionState` stores an optional `EpiphanyThreadState`, and `Session` exposes thin async accessors over it in [state/session.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/state/session.rs:36) and [session/mod.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/session/mod.rs:1240).
- prompt injection: `build_initial_context` reads the optional state and adds a bounded `<epiphany_state>` developer fragment; the renderer now includes durable investigation-checkpoint and planning sections when authoritative state carries them, so a resumed agent can see whether source gathering was banked and what captures/backlog/drafts exist without treating them as adopted objectives in [session/mod.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/session/mod.rs:2434), [prompt.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/prompt.rs:33), and [prompt.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/prompt.rs:127).
- prompt injection: `build_initial_context` reads the optional state and adds a bounded `<epiphany_state>` developer fragment; the renderer now lives beside the typed state model and includes durable investigation-checkpoint and planning sections when authoritative state carries them, so a resumed agent can see whether source gathering was banked and what captures/backlog/drafts exist without treating them as adopted objectives in [session/mod.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/session/mod.rs:2434), [prompt.rs](E:/Projects/EpiphanyAgent/epiphany-state-model/src/prompt.rs:33), and [prompt.rs](E:/Projects/EpiphanyAgent/epiphany-state-model/src/prompt.rs:127).
- per-turn persistence: normal turn setup persists one `RolloutItem::EpiphanyState` after the `TurnContext` when state exists in [session/mod.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/session/mod.rs:2677).
- client read hydration: app-server thread views attach live or reconstructed `thread.epiphanyState` in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4706) and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4822).
- explicit distillation proposals: app-server routes read-only `thread/epiphany/distill` through a loaded-thread handler into `epiphany-core` so one explicit observation can become a patch candidate without mutating state; tool/command/shell/model sources now get typed evidence kinds and bounded salient-output summaries instead of dumping raw output into durable state in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4358) and [distillation.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/distillation.rs:28).
- explicit map/churn proposals: app-server routes read-only `thread/epiphany/propose` through a loaded-thread handler into `epiphany-core` so verified observations with code refs and accepting recent evidence can be selected explicitly or auto-selected as a bounded path cluster, prioritize the strongest selected observation, focus or extend architecture graph nodes, rescue unanchored graph nodes through strict semantic overlap, carry linked dataflow nodes and incident graph edges into the frontier, update churn candidates with match-kind-aware map-delta judgment and pressure, and still avoid mutation until promotion in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4429) and [proposal.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/proposal.rs:115).
- explicit promotion gates: app-server routes `thread/epiphany/promote` through a loaded-thread handler into `epiphany-core` policy evaluation, rejects failed verifier evidence without mutation, applies accepted candidates through the same durable update path, and now treats medium/high/expanded/broadening/semantic churn deltas as needing explicit rationale plus stronger verifier evidence whose kind is token-matched rather than substring-matched; the same gate also validates durable investigation-checkpoint packets, including linked evidence ids, before they can be inked into state in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4501) and [promotion.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/promotion.rs:33).
- 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).
- explicit state updates: app-server routes `thread/epiphany/update` through `epiphany-codex-bridge` mutation policy, which 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, then asks `CodexThread` only to persist the already-mutated typed state snapshot as a `RolloutItem::EpiphanyState` in [mutation_service.rs](E:/Projects/EpiphanyAgent/epiphany-codex-bridge/src/mutation_service.rs:126), [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:348), 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: the `scene` view lens reads the same live/stored Epiphany state view used by `thread/read` and adapts the core scene projection. `epiphany-core` derives the 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 [scene.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/surfaces/scene.rs:1).
- job/progress reflection: the `jobs` view lens reads the same thread view as scene, optionally fills retrieval state for loaded threads with no Epiphany state, and adapts core-derived non-authoritative progress slots for retrieval indexing, graph remap, invariant verification, and bound specialist work. Durable `job_bindings` now act as thin Epiphany-owned authority slots with binding id, kind, scope, owner role, authority scope, linked ids, and optional blocking reason only. Runtime job ids project from `runtime_links` through core job view policy; old `agent_jobs` bindings are sealed instead of live-crawled in [jobs.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/surfaces/jobs.rs:1).
@ -37,7 +37,7 @@ The spine exists in more than twenty-five live paths:
- CRRC coordinator recommendation: the `crrc` view lens reuses 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 calls pure `epiphany-core` CRRC policy to return 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 [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4661), [crrc.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/surfaces/crrc.rs:60), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4546).
- role ownership projection: the `roles` view lens 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. App-server adapts concrete protocol jobs and thread state, while the pure role-board projection policy now lives in [role_board.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/surfaces/role_board.rs:70).
- 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 and seals legacy `agent_jobs` bindings, then adapts worker JSON through core role-result interpretation in [role_result.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/surfaces/role_result.rs:45), projecting structured output as a reviewable finding with verdict, summary, next safe move, inspected files, frontier/evidence ids, optional `statePatch`, optional `selfPatch`, coordinator `selfPersistence` review, and errors without mutation, promotion, scheduling, or hidden continuation. `selfPatch` is a Ghostlight-shaped lane-memory request, not project truth: core 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, and uses core acceptance bundle policy to build the audit observation, evidence, and typed receipt. 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. 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.
- 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. The base `<epiphany_state>` doctrine reinforces the Perfect Machine rule through [prompt.rs](E:/Projects/EpiphanyAgent/epiphany-state-model/src/prompt.rs:31) and [epiphany_doctrine.md](E:/Projects/EpiphanyAgent/epiphany-state-model/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: the `coordinator` view lens composes role, pressure, reorient, CRRC, role result, and reorient result signals, and returns one current action without mutation. App-server assembles host/protocol inputs, but the pure fixed-lane decision ladder now lives in `epiphany-core` in [coordinator.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/surfaces/coordinator.rs:132). 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: the old Python runner was cut with the `agent_jobs` path. The replacement must be native Rust/CultCache/CultNet and complete heartbeat-owned runtime-spine job results with operator-safe artifacts.
@ -45,11 +45,11 @@ The spine exists in more than twenty-five live paths:
- 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 heartbeat bindings through typed runtime-spine job results, seals legacy `agent_jobs` 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, uses core acceptance bundle policy to build accepted observation/evidence/receipt payloads plus optional scratch and durable investigation-checkpoint updates, 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), [role_result.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/surfaces/role_result.rs:150), 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).
- retrieval/indexing: app-server routes `thread/epiphany/retrieve` and `thread/epiphany/index` through `epiphany-codex-bridge::retrieve`, which reads only host cwd/Codex-home facts from `CodexThread` and calls native `epiphany-core` retrieval/indexing in [retrieve.rs](E:/Projects/EpiphanyAgent/epiphany-codex-bridge/src/retrieve.rs:42), [retrieve.rs](E:/Projects/EpiphanyAgent/epiphany-codex-bridge/src/retrieve.rs:57), and [retrieve.rs](E:/Projects/EpiphanyAgent/epiphany-codex-bridge/src/retrieve.rs:69).
The thick Epiphany-owned implementation is mostly outside vendored Codex now:
- prompt rendering lives in [epiphany-core/src/prompt.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/prompt.rs:33), including the always-rendered operating-discipline section in [prompt.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/prompt.rs:26).
- prompt rendering lives in [epiphany-state-model/src/prompt.rs](E:/Projects/EpiphanyAgent/epiphany-state-model/src/prompt.rs:33), including the always-rendered operating-discipline section in [prompt.rs](E:/Projects/EpiphanyAgent/epiphany-state-model/src/prompt.rs:26).
- generic rollout replay for stored-thread reads lives in [epiphany-core/src/rollout.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/rollout.rs:38).
- retrieval and indexing live in [epiphany-core/src/retrieval.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/retrieval.rs:144) and [retrieval.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/retrieval.rs:160), with summary-only freshness reads in [retrieval.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/retrieval.rs:100).
- deterministic observation distillation lives in [epiphany-core/src/distillation.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/distillation.rs:28).
@ -64,7 +64,7 @@ flowchart TD
A["Loaded Codex Session"] --> B["SessionState.epiphany_state"]
B --> C["build_initial_context"]
C --> D["EpiphanyStateInstructions wrapper"]
D --> E["epiphany-core render_epiphany_state"]
D --> E["epiphany-state-model render_epiphany_state"]
E --> EA["Operating Discipline anti-Jenga section"]
EA --> F["<epiphany_state> developer fragment"]
F --> G["Model/tool turn runs in normal Codex loop"]
@ -130,15 +130,15 @@ flowchart TD
AE --> AF{"Accepted?"}
AF -->|"No"| AG["Return reasons, no state mutation"]
AF -->|"Yes"| Z
R --> T["CodexThread.epiphany_retrieve"]
S --> U["CodexThread.epiphany_index"]
Z --> ZA["CodexThread.epiphany_update_state"]
AJL --> AJLA["CodexThread.epiphany_launch_job"]
R --> T["epiphany-codex-bridge retrieve_thread_epiphany"]
S --> U["epiphany-codex-bridge index_thread_epiphany_retrieval_state"]
Z --> ZA["epiphany-codex-bridge apply_epiphany_state_update_to_thread"]
AJL --> AJLA["epiphany-codex-bridge launch_epiphany_job_on_thread"]
QRLQ --> QRLA["Map checkpoint + reorient verdict into fixed reorient-worker launch"]
QRLA --> AJLA
AJLA --> AJLB["Persist authority-slot jobBinding + runtimeLink"]
AJLB --> ZD
AJI --> AJIA["CodexThread.epiphany_interrupt_job"]
AJI --> AJIA["epiphany-codex-bridge interrupt_epiphany_job_on_thread"]
AJIA --> AJIB["Interrupt backend worker(s) + clear bound backend from jobBinding"]
AJIB --> ZD
ZA --> ZB["Apply typed patch and increment revision"]
@ -310,7 +310,7 @@ There is now one deliberately unbounded-in-spirit but tiny-in-text exception: th
It then builds the normal developer bundle. After collaboration-mode instructions, it checks `epiphany_state.as_ref()` and pushes `EpiphanyStateInstructions::from_state(...).render()`.
`EpiphanyStateInstructions` is only a Codex adapter. It calls `epiphany_core::render_epiphany_state(state)` and wraps the result in the protocol tags.
`EpiphanyStateInstructions` is only a Codex adapter. It calls the `codex_protocol` re-export of the state-model `render_epiphany_state(state)` and wraps the result in the protocol tags.
`render_epiphany_state` always emits:
@ -324,9 +324,9 @@ Code refs:
- [session/mod.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/session/mod.rs:2447)
- [session/mod.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/session/mod.rs:2509)
- [context/epiphany_state_instructions.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/context/epiphany_state_instructions.rs:7)
- [epiphany-core/src/prompt.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/prompt.rs:26)
- [epiphany-core/src/prompt.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/prompt.rs:33)
- [epiphany-core/src/prompt.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/prompt.rs:43)
- [epiphany-state-model/src/prompt.rs](E:/Projects/EpiphanyAgent/epiphany-state-model/src/prompt.rs:26)
- [epiphany-state-model/src/prompt.rs](E:/Projects/EpiphanyAgent/epiphany-state-model/src/prompt.rs:33)
- [epiphany-state-model/src/prompt.rs](E:/Projects/EpiphanyAgent/epiphany-state-model/src/prompt.rs:43)
### Output
@ -415,7 +415,7 @@ For `thread/read`, `read_thread_view` loads the persisted thread view and option
- if the thread is loaded, use `live_thread_epiphany_state`.
- otherwise, read rollout items from disk and reconstruct the latest surviving Epiphany state with `latest_epiphany_state_from_rollout_items`.
`live_thread_epiphany_state` reads `CodexThread.epiphany_state()`. If state exists but has no retrieval summary, it calls `thread.epiphany_retrieval_state().await` and backfills the summary into the returned API object. That backfill is for the view; it is not a durable rollout write.
`live_thread_epiphany_state` reads `CodexThread.epiphany_state()`. If state exists but has no retrieval summary, it calls `epiphany-codex-bridge::thread_epiphany_retrieval_state` and backfills the summary into the returned API object. That backfill is for the view; it is not a durable rollout write.
Code refs:
@ -468,10 +468,10 @@ The app-server handler:
3. rejects zero limits.
4. requires the thread to be loaded.
5. clamps limit to Epiphany bounds.
6. calls `CodexThread.epiphany_retrieve`.
6. calls `epiphany-codex-bridge::retrieve_thread_epiphany`.
7. maps core results into app-server protocol DTOs.
`CodexThread.epiphany_retrieve` snapshots the thread config, extracts `cwd` as workspace root, gets `codex_home`, and runs `epiphany_retrieval::retrieve_workspace` in `spawn_blocking`.
`retrieve_thread_epiphany` snapshots the host config, extracts `cwd` as workspace root, gets Codex home from the host seam, and runs `epiphany_core::retrieve_workspace` in `spawn_blocking`.
`epiphany-core::retrieve_workspace`:
@ -541,10 +541,10 @@ The app-server handler:
1. parses the thread id.
2. requires the thread to be loaded.
3. calls `CodexThread.epiphany_index(force_full_rebuild)`.
3. calls `epiphany-codex-bridge::index_thread_epiphany_retrieval`.
4. maps the returned core retrieval state into an app-server index summary response.
`CodexThread.epiphany_index` snapshots `cwd`, reads `codex_home`, and runs `epiphany_retrieval::index_workspace` in `spawn_blocking`.
`index_thread_epiphany_retrieval_state` snapshots `cwd`, reads Codex home from the host seam, and runs `epiphany_core::index_workspace` in `spawn_blocking`.
`epiphany-core::index_workspace`:
@ -595,7 +595,7 @@ It must stay light. It should measure the catalog, not rewrite the catalog. If t
### Mechanism
`live_thread_epiphany_state` calls `thread.epiphany_retrieval_state().await`. That method runs `retrieval_state_for_workspace`, which:
`live_thread_epiphany_state` calls `epiphany-codex-bridge::thread_epiphany_retrieval_state`. That bridge helper runs `retrieval_state_for_workspace`, which:
1. checks the workspace exists.
2. loads backend config.
@ -653,14 +653,14 @@ The app-server handler:
1. parses the thread id.
2. requires the thread to be loaded.
3. maps the app-server patch DTO into `EpiphanyStateUpdate`.
4. calls `CodexThread.epiphany_update_state`.
4. calls `epiphany-codex-bridge::apply_epiphany_state_update_to_thread`.
5. computes `changedFields` from the accepted patch surface.
6. projects the just-written state through `client_visible_live_thread_epiphany_state`, which reuses the `thread/read` live-state helper and backfills retrieval summary metadata when needed.
7. returns the client-visible `EpiphanyThreadState` plus response-level `revision` and `changedFields`.
8. emits `thread/epiphany/stateUpdated` with `source: "update"`, event-level `revision`, typed `changedFields`, and the same client-visible updated state.
9. reports empty patches and revision mismatches as invalid requests before notification.
`CodexThread.epiphany_update_state`:
`apply_epiphany_state_update_to_thread`:
1. rejects an empty patch.
2. reads the current reference turn id when one exists.
@ -920,7 +920,7 @@ The app-server handler:
4. returns `accepted: false`, reasons, and `epiphanyState: null` when policy rejects.
5. appends verifier evidence to the patch evidence when policy accepts.
6. maps the patch into `EpiphanyStateUpdate`.
7. calls `CodexThread.epiphany_update_state`.
7. calls `epiphany-codex-bridge::apply_epiphany_state_update_to_thread`.
8. projects the just-written state through `client_visible_live_thread_epiphany_state`, matching the `thread/read` live-state view and retrieval-summary backfill.
9. returns the client-visible `EpiphanyThreadState` plus response-level `revision` and `changedFields`.
10. emits `thread/epiphany/stateUpdated` with `source: "promote"`, event-level `revision`, and typed `changedFields` only for the accepted path; `Evidence` is included even if the accepted patch had no patch evidence because verifier evidence was appended to the durable state.
@ -958,7 +958,7 @@ Rejected promotions return reasons and no state. Accepted promotions return the
### Invariant
Promotion is a gate, not independent persistence. It does not write rollout items directly, it does not mutate state on rejection, and rejected promotions emit no state update notification. Accepted promotions still go through `CodexThread.epiphany_update_state`, so the red pen remains one tool, not three tools in a coat.
Promotion is a gate, not independent persistence. It does not write rollout items directly, it does not mutate state on rejection, and rejected promotions emit no state update notification. Accepted promotions still go through `epiphany-codex-bridge::apply_epiphany_state_update_to_thread`, so the red pen remains one tool, not three tools in a coat.
## Flow 13: Scene View Lens Control Flow
@ -1509,7 +1509,7 @@ Current tests cover the landed flows at useful seams:
- deterministic map/churn proposal in `epiphany-core`.
- verifier-backed promotion policy in `epiphany-core`.
- map/churn promotion validation for evidence-backed state replacements in `epiphany-core`.
- typed state-update patch application in `codex-core`.
- typed state-update patch application in `epiphany-codex-bridge` plus host persistence in `codex-core`.
- retrieval ranking, fallback, stale manifest detection, and mocked Qdrant/Ollama indexing in `epiphany-core`.
- app-server protocol serde for `thread/epiphany/retrieve`, `thread/epiphany/index`, `thread/epiphany/distill`, `thread/epiphany/propose`, `thread/epiphany/promote`, `thread/epiphany/update`, `thread/epiphany/stateUpdated`, and `thread/epiphany/jobsUpdated`.
- focused app-server coverage for sealed `thread/epiphany/jobsUpdated` behavior and heartbeat-backed job projection.

View file

@ -85,10 +85,11 @@ The landed machine now has:
- old Python dogfood runner cut; replacement must be native Rust/CultCache/CultNet over heartbeat/runtime-spine receipts
- first auditable Phase 6 fixed-lane coordinator runner through native `epiphany-mvp-coordinator`, producing coordinator summary, JSONL steps, rendered snapshots, transcript, stderr, runtime-spine status, native runtime job/result receipts for launched work, and final next-action artifacts while keeping semantic findings review-gated by default
- first Codex-independent native runtime vertebra through `epiphany-runtime-spine`, storing runtime identity, sessions, jobs, job results, and events as typed CultCache MessagePack documents, opening/completing native jobs, projecting job-result counts, and emitting a framed CultNet hello message for the native contract
- first Rust CultMesh local-node integration: `CultLib` now owns `crates/cultmesh-rs`, and `epiphany-core` depends on that CultMesh surface for local `EpiphanyCultMeshStatusEntry`, `EpiphanyCultMeshVersePolicyEntry`, and `EpiphanyCultMeshGlobalRoomPolicyEntry` round-trip smokes instead of treating raw CultNet as the ergonomic app-facing database layer
- live-specialist runner must be rebuilt around heartbeat/runtime-spine result receipts
- first Phase 6 Aquarium operator shell extracted to sibling repo `E:\Projects\EpiphanyAquarium`, a Tauri v2 + React/WebGL client over the existing status bridge, dogfood artifacts, and GUI action artifacts, with its own distilled interface state/memory/doctrine plus durable checkpoint preparation, bounded status/coordinator artifact buttons, fixed modeling/verification/reorient launch and read-back buttons, and explicit review-gated reorient acceptance
- first Unity editor/runtime bridge through native `epiphany-unity-bridge`, native `epiphany-unity-bridge-smoke`, and the GUI Inspect Unity action, resolving exact project-pinned editors and writing runtime artifacts while refusing wrong or missing versions
- prompt-file ownership for major Epiphany prompt surfaces: rendered state intro/doctrine lives under `epiphany-core/src/prompts/`, while modeling/Body, research/Eyes, implementation/Hands, verification/Soul, reorientation/Life, coordinator/Self, and CRRC templates live in `vendor/codex/codex-rs/app-server/src/prompts/epiphany_specialists.toml`
- prompt-file ownership for major Epiphany prompt surfaces: rendered state intro/doctrine lives under `epiphany-state-model/src/prompts/` beside the typed state document renderer, while modeling/Body, research/Eyes, implementation/Hands, verification/Soul, reorientation/Life, coordinator/Self, and CRRC templates live in `vendor/codex/codex-rs/app-server/src/prompts/epiphany_specialists.toml`
- prompt-level anti-Greenspun guardrails: Epiphany keeps Codex's useful operator discipline and requires a bounded research/scout check before agents invent bespoke versions of known algorithms, parsers, schedulers, renderers, protocols, storage layers, security mechanisms, or workflow engines
- live Phase 6 reorientation app-server smoke coverage through native `epiphany-phase6-reorient-smoke`
- live Phase 6 MVP status smoke coverage through native `epiphany-mvp-status-smoke`
@ -115,6 +116,8 @@ These boundaries are more important than the individual method names:
- CultCache documents are the data model. CultNet is the wire for
Epiphany-controlled subsystems.
- CultMesh is the preferred ergonomic runtime/database surface over local CultCache plus CultNet contracts. Epiphany should start local Verse, dream, status, and operator-store work through CultMesh unless the task is explicitly implementing a lower-level CultMesh/CultNet/CultCache primitive.
- Verse boundaries are three-tiered: `epiphany-internal` owns private sub-agent typed state, `gamecult-local` owns trusted LAN/Yggdrasil-tunnel sharing with other GameCult projects, and `epiphany-global` owns untrusted public dream surfaces plus topic-specific threaded rooms for Face posts. Do not route private state through the local-area or global Verse.
- JSON is allowed only for schema description, hostile/external ingress before
immediate typed parsing, sealed forensic artifacts, or named quarantine
experiments with an expiration path. Generic `serde_json::Value` must not be
@ -192,6 +195,15 @@ The active priority is now the Codex starvation plan:
feature surfaces until the auth/model spine and JSON contamination inventories
exist and the first typed-document replacement cut is underway.
The distributed dreaming direction is now concrete in
`notes/epiphany-cultmesh-dreaming-roadmap.md`. It is not a license to add a
new networked thought hose beside the starvation work. It is the target shape
for the CultNet liberation path: public dreams are separately authored typed
documents; private state remains local; foreign dreams are external thought
weather until a reviewed local adoption receipt digests them. The first
implementation cut is schema plus local CultCache dream store, not public mesh
fanout.
The next unknowns are:
- what exact vendored Codex crates, files, config, and runtime paths are needed
@ -211,6 +223,12 @@ The next unknowns are:
app-server/plugin organs
- how to make Aquarium and CLI status use CultNet contracts instead of
`thread/epiphany/*`
- how to define `EpiphanyDream`, dream lineage, ingress receipts, and adoption
receipts as typed documents without letting dreams mutate private state or
project truth by import
- how to expose an `epiphany-local` dream store first, then a public
`epiphany-dreams` Verse with leases, shard ownership, provenance, and
anti-repetition cooling
- what bridge surface is sufficient for the next Aetheria run: named Unity operations, Aetheria-side editor probes, GUI environment status, Rider context capture, and a Rider plugin MVP
- how much controlled runtime/editor access the richer Unity bridge gives implementation and verification lanes before deeper engine probes need another slice
- how the landed watcher-backed invalidation telemetry should be consumed without turning freshness into a secret worker

View file

@ -0,0 +1,188 @@
# Epiphany Swarm Readiness Plan
This is the direction of travel before live fire.
The local run path proves that the current machine can be started and inspected.
It does not prove the swarm is ready to run unattended. Do not confuse a working
starter switch with a clean engine bay. That mistake has already been punished
elsewhere, and VoidBot has kindly provided the scorch marks.
## Objective
Get Epiphany ready for cautious live operation over the next few days without
building architectural Jenga:
- typed CultCache documents remain the state authority
- CultNet is the native wire for Epiphany-owned subsystems
- Codex remains the retained auth/model transport compatibility organ
- Aquarium and Face are reflectors/mouths, not hidden sources of truth
- heartbeats create opportunities, not authority
- swarm speech and action stay gated until scheduling, memory, repetition, and
review boundaries are coherent
## VoidBot Lessons To Carry Forward
VoidBot's ordinary spine is not the rotten part. Its durable lessons are:
- Discord ingestion, worker jobs, provider lanes, RAG, Postgres, Qdrant, and
typed CultCache self-state can be conceptually sound when each owns one job.
- The unstable organ was the repo Face swarm loop: scheduling, prompt policy,
identity, public speech, governance, dispatch, proposal behavior, and
repetition control packed into one prompt-and-parser machine.
- A repo-controlled pause flag is a real brake. If a swarm subsystem is under
teardown, runners must fail closed instead of routing around the pause.
- One CTB-style initiative scheduler beats one task per agent. The wall-clock
task supplies pulses; typed initiative state chooses turns.
- Heat changes recovery pressure. It must not fast-forward time, bypass active
turn freeze, or create overlapping thoughts.
- Cooldown starts after completion, not after queueing.
- Stale active turns need explicit recovery receipts, or one dead worker claim
freezes a Face forever.
- Public speech needs parent-side eligibility checks. Prompts alone cannot
prevent repeated openings, stale topics, scheduler labels leaking into prose,
or work requests masquerading as banter.
- Memory must have phases: short-term residue, incubation, durable memory,
revision, retirement, crystallization, and sleep-owned maintenance.
- Agents propose typed operations. State services validate, normalize, dedupe,
and write. Whole-state JSON editing is not a mutation path.
- Qdrant/vector recall is a rebuildable resonance cache, not canonical memory.
- Face affect is state, not vibes: needs, social bonds, status reads, and mood
dimensions must survive typed projection and MCP/CultNet inspection.
## Current Mechanism
Epiphany currently has the right ingredients, but not yet the final live swarm
shape:
- `tools/epiphany_local_run.ps1` can build and smoke the compatibility shell.
- `epiphany-runtime-spine` owns typed runtime identity/session/job/result/event
documents and advertises CultNet contracts.
- heartbeat state owns role dossiers, initiative, heat, active-turn freeze,
sleep, rumination, appraisals, and derived reactions.
- role memory accepts reviewed `selfPatch` petitions into typed Ghostlight-like
dossiers.
- coordinator/CRRC/role/status surfaces exist, but the practical local runner
still goes through Codex app-server JSON-RPC for operator reads/actions.
- Face, Discord, Rider, Unity, Void memory, and repo birth bridges are typed
surfaces or artifacts, but they are not yet an integrated live swarm loop.
The machine is pointed toward live use. It is not cleared for unattended swarm
operation.
## Invariants
- One state authority per document kind.
- One scheduler for standing initiative.
- One explicit brake for swarm operation.
- No lane receives a new heartbeat while its previous turn is running.
- No model output rewrites durable state directly.
- No public speech without typed eligibility and receipt checks.
- No cross-repo rummaging; swarm needs move coordinator-to-coordinator through
visible typed messages and callbacks.
- No hidden automatic semantic acceptance.
- No prompt-only governance for things that need state, receipts, or gates.
- No broad live scheduler until the local runner can show the typed surfaces
that explain what it would do and why.
## Readiness Gate
Before live fire, Epiphany needs these gates closed:
1. **Operator Run Path**
- Keep `tools/epiphany_local_run.ps1` as the human entrypoint for now.
- It must keep printing a concise verdict.
- It must keep artifacts operator-safe by default.
- It must make Codex app-server dependency obvious as compatibility, not
architecture.
2. **Typed Swarm Brake**
- Add an Epiphany-owned pause document for swarm/heartbeat/live Face
operation.
- Runners must fail closed when the document is missing, malformed, or
paused for a teardown reason, according to the surface being protected.
- The brake must be inspectable through Aquarium/status.
3. **Initiative Scheduler Boundary**
- Keep one heartbeat scheduler for lanes/Faces/system organs.
- Heat is a multiplier on recovery pressure, not a time machine.
- Active-turn freeze and completion-gated cooldown are hard invariants.
- Stale active-turn recovery must write typed receipts.
4. **Memory Lifecycle**
- Keep role-local memory writes as reviewed `selfPatch` operations.
- Add explicit lifecycle operations where missing: revise, retire,
crystallize, prune short-term residue, and merge incubation support.
- Sleep/maintenance may propose lifecycle operations; it must not silently
rewrite role identity.
5. **Public Face Safety**
- Face speech eligibility must be parent-side state policy, not prompt hope.
- Track recent outputs, repeated topics, repeated openings, target/channel
saturation, and action kind.
- Work-shaped asks route to coordinator/Bifrost-style governance; banter
does not secretly become work dispatch.
6. **CultNet Operator Surface**
- Move the local status/coordinator loop toward native CultNet snapshots,
intents, and receipts.
- Codex JSON-RPC may remain as the compatibility projection while the wall is
being evacuated.
- The next run path should consume typed documents first and only use
app-server JSON-RPC where no native route exists yet.
7. **Audit And Review**
- Every automatic or semi-automatic action must produce a typed receipt.
- Semantic findings remain review-gated.
- Operator artifacts must seal direct thought, raw transcripts, and long
prompt-shaped notes by default.
## Ranked Next Cuts
Keep:
- typed CultCache state
- runtime-spine documents and CultNet catalog
- heartbeat initiative physiology
- reviewed role memory patches
- local run/status/coordinator artifacts
- Codex auth/model transport reliquary
Cut:
- schema/catalog language that treats legacy JSON-RPC routes as native authority
- public or operator outputs that dump prompt slabs as if they were status
- any new live runner that bypasses typed pause, receipt, or review gates
Collapse:
- local runner status/coordinator result shaping into one operator projection
helper if duplication grows
- Face speech receipts and eligibility into one typed surface before high-rate
speech returns
Split:
- heartbeat opportunity from action authority
- Face affect/state from speech generation
- scheduler metadata from model-facing prose
- vector recall from canonical memory
Rebuild:
- any swarm loop that depends on a single prompt to own scheduling, identity,
public speech, governance, proposal behavior, and repetition control
## Immediate Work Queue
1. Add typed swarm pause/brake state and status projection.
2. Add stale active-turn recovery receipts to heartbeat state if the current
heartbeat store cannot prove recovery cleanly.
3. Add a Face output audit surface before increasing public speech cadence.
4. Add memory lifecycle operations that match VoidBot's useful pattern without
porting its TypeScript state shape wholesale.
5. Move one local-run read path from Codex JSON-RPC compatibility to CultNet
snapshot/receipt consumption.
The correct posture is patient pressure. We are not rushing to live fire. We are
aiming the barrel, checking the chamber, and making sure the thing does not try
to become a parliament of prompts the moment nobody is watching.

View file

@ -0,0 +1,49 @@
# Eyes CultNet Contracts
Objective: make Eyes the evidence ingress guardian. Body controls whether the
machine may touch the repo or substrate; Eyes controls whether a claim has been
looked at well enough to become citable evidence for another organ.
## Authority Map
- Owner: Eyes owns source-grounded evidence packets.
- Inputs: Body-granted source access, retrieval/index requests, search queries,
source refs, claimed facts, uncertainty requirements, and requested evidence
scope from any organ.
- Outputs: Eyes evidence reviews, source lookup receipts, evidence packets, and
evidence refusal receipts.
- Derived state: raw snippets, search hits, retrieved chunks, logs, web pages,
and source excerpts are not citable truth until Eyes packages provenance,
uncertainty, and limits.
- Forbidden paths: Imagination, Hands, Face, Mind, Self, Body, Soul, Life, raw
workers, and compatibility JSON-RPC routes must not treat raw retrieved
material as inspected evidence without an Eyes receipt.
- Shared path: any organ that needs truth asks Eyes for a source-grounded
packet. Soul may later verify the packet against invariants, but Eyes owns the
first act of looking.
- Deletion line: Body access receipts are not evidence receipts. A repo read
grant says the substrate may be touched; it does not say the claim is known.
## Contract Families
- `epiphany.eyes.evidence_request`: request for source-grounded evidence.
- `epiphany.eyes.evidence_review`: Eyes decision about grounding, uncertainty,
and missing looking.
- `epiphany.eyes.source_lookup_receipt`: proof of what was searched or
inspected under which Body grant.
- `epiphany.eyes.evidence_packet`: citable packet with provenance,
uncertainty, and source refs.
- `epiphany.eyes.evidence_refusal_receipt`: proof that Eyes refused to certify a
claim.
## Neighboring Gates
- Body grants repo/substrate access.
- Eyes turns inspected material into evidence.
- Imagination projects evidence into possible scenes and futures.
- Mind decides whether evidence or thought mutates persistent state.
- Soul verifies invariants and falsifies claims before the machine calls them
safe.
Eyes is not the whole truth organ. It is the looking organ. It keeps the machine
from citing fog with line numbers.

View file

@ -33,15 +33,69 @@ remembers doctrine; the branch remembers the blade.
## Current Orientation
- Do not copy exact branch or HEAD from this note. Run `git status --short --branch` and `git log --oneline -5`.
- The repo now has a one-command local operator path: `.\tools\epiphany_local_run.ps1`. Default mode is `smoke`; it builds the retained Codex app-server compatibility edge, builds native Epiphany operator binaries, runs the coordinator smoke, and writes launcher artifacts under `.epiphany-run/` plus coordinator artifacts under `.epiphany-dogfood/`. Use `-Mode status`, `-Mode plan`, or `-Mode run -MaxSteps 4` for the other current entrypoints; add `-ThreadId <id>` and `-Workspace <path>` to inspect or continue an existing thread. `status`, `plan`, and `smoke` do not spend model calls; `run` builds and uses `epiphany-openai-runtime`.
- The active foundation directive is now `notes/codex-starvation-and-cultnet-liberation-plan.md`. The previous Codex app-server control-plane rebuild made Epiphany less rotten inside Codex, but that is not the destination. Epiphany must become a native CultCache/CultNet runtime while remaining an honest modified Codex-derived backend for subscription auth/model use. Codex is a compatibility reliquary for OpenAI subscription auth and model transport, not the host brain and not a mask for impersonation.
- `notes/epiphany-cultmesh-dreaming-roadmap.md` is the new concrete design for distributed Epiphany dreaming. The invariant is hard: private state stays local; public dreams are separately authored typed documents distributed through CultMesh/CultNet; foreign dreams are thought weather until a reviewed local adoption receipt digests them. The first code slice should be dream schemas plus a local CultMesh-backed store, not public fanout.
- CultMesh is now the preferred Rust abstraction for that local store work. The Rust port lives in `E:\Projects\CultLib\crates\cultmesh-rs`, and Epiphany now depends on it directly from `epiphany-core`. The first integration is deliberately small: `EpiphanyCultMeshStatusEntry`, `EpiphanyCultMeshVersePolicyEntry`, and `EpiphanyCultMeshGlobalRoomPolicyEntry` round-trip through `.epiphany-smoke/cultmesh/epiphany-local.ccmp` using `cargo run --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-cultmesh-smoke`. Future local dream/status/Verse work should start from CultMesh, not raw CultNet, unless the task is polishing the CultMesh internals themselves. Preserve the three Verse tiers: `epiphany-internal` for private sub-agent typed state, `gamecult-local` for trusted LAN/Yggdrasil-tunnel GameCult sharing, and `epiphany-global` for untrusted public dreams plus topic-specific threaded public rooms for Face posts.
- `notes/codex-auth-spine-inventory.md` is the source-grounded keeper list for that reliquary. Corrected compliance invariant: Codex-compatible auth identity stays anchored in vendored Codex-derived auth machinery. `epiphany-openai-auth-spine` is now a thin Epiphany-named boundary over `codex-login`, not a clean-room clone of credential storage or token refresh. Codex apps, skills, marketplace, plugin UX, broad app-server workflow, and JSON-RPC sprawl remain cuttable because they are not subscription-auth legitimacy.
- Edge JSON is allowed for schema description, hostile ingress before immediate typed parsing, sealed forensic artifacts, or named quarantine experiments. When both subsystems are ours, runtime data must remain typed CultCache documents and move over CultNet typed contracts. Generic `serde_json::Value` in worker launch/result/selfPatch/runtime flow is contamination until classified and replaced.
- `notes/epiphany-swarm-readiness-plan.md` is the current live-fire direction gate. The local runner proves startup/status/coordinator wiring; it does not authorize unattended swarm operation. Carry VoidBot's lessons forward: one CTB-style initiative scheduler, typed pause/brake, active-turn freeze, cooldown after completion, stale active-turn recovery receipts, parent-side speech eligibility, memory lifecycle phases, and typed operation proposals instead of prompt-owned governance.
- `epiphany-openai-runtime` worker result ingress now parses assistant output directly into typed role/reorient ingress structs selected from the durable launch document, stores nested `statePatch` and `selfPatch` as typed MessagePack in `EpiphanyRuntimeRoleWorkerResult`, and no longer passes worker result data through generic `serde_json::Value`. JSON survives there only as the OpenAI text-output edge before typed deserialization.
- Agent self-memory review/application now has typed `AgentSelfPatch` core APIs. JSON-value selfPatch functions are compatibility wrappers for CLI/artifact ingress; native callers should parse once at the edge and then call `review_agent_self_patch_document` / `apply_agent_self_patch_document`. Role-result self-persistence no longer exports a JSON-value reviewer; the live API is `review_role_self_patch_document`.
- Durable Ghostlight role dossiers now store typed relationship, event, scene, and perceived-overlay documents instead of `Vec<serde_json::Value>`, and the durable dossier structs no longer preserve flattened unknown `extra` maps. `AgentSelfPatch.extra` remains deliberately, but only as an ingress rejection ledger for unexpected patch fields.
- The native state ledger is closed typed state now: branch and evidence records no longer preserve flattened `extra` maps. Evidence entries are explicit `ts` / `type` / `status` / `note` / optional branch records.
- `epiphany-openai-codex-spine` no longer uses Codex's Responses client, model-provider layer, `codex-api`, `codex-model-provider`, `codex-model-provider-info`, or `codex-protocol` for outbound model requests. It reaches `codex-login` only through `epiphany-openai-auth-spine` so credential loading/refresh stays Codex-compatible while request/status/event/runtime surfaces stay typed Epiphany documents.
- `notes/epiphany-memory-graph-unified-plan.md` is the current memory architecture direction drawn from `Fractal Domains And The Cache That Bites` plus the VoidBot memory scar tissue. The repo semantic architecture/dataflow graph and agent memory are one `EpiphanyMemoryGraph` substrate with repo, role, short-term, incubation, agency, candidate-intervention, and evidence profiles. The first native skeleton is landed: `epiphany-state-model` owns shared graph document types, `epiphany-core::memory_graph` owns stable ids, validation, freshness, context cuts, and CultCache persistence, and `epiphany-memory-graph` can status/validate/context/smoke the store without Qdrant. Qdrant is a contribution cache for typed graph documents, not truth. `notes/repo-fractal-dataflow-cache-plan.md` and `notes/agent-memory-fractal-cache-plan.md` are profile plans, not rival organs.
- Durable Epiphany state types now live in root crate `epiphany-state-model`. `codex-protocol` re-exports them only so old rollout/API callers keep compiling; do not add new durable `Epiphany*` state structs under `vendor/codex`. `epiphany-core` imports the native model directly and no longer depends on `codex-protocol`. Pressure derives from native `EpiphanyTokenUsageSnapshot`; Codex token telemetry is mapped at the bridge. Codex rollout/event reconstruction now lives in `codex-core::epiphany_rollout`, where the Codex `RolloutItem` / `EventMsg` types belong. Exact retrieval path matching is native to `epiphany-core`, so `epiphany-core` has no direct vendored Codex crate dependency.
- Retrieval's Qdrant boundary has a typed point payload now. Qdrant's HTTP requests, responses, and acknowledgements are JSON wire edges; Epiphany's retrieval facts (`path`, `line_start`, `line_end`, `excerpt`) are `QdrantPointPayload` before serialization/deserialization, not a loose payload map.
- Durable Epiphany state types now live in root crate `epiphany-state-model`. `codex-protocol` re-exports them only so old rollout/API callers keep compiling; do not add new durable `Epiphany*` state structs under `vendor/codex`. The deterministic `<epiphany_state>` prompt renderer also lives beside that typed model now, with prompt text under `epiphany-state-model/src/prompts/`; `epiphany-core` only re-exports it for compatibility. `epiphany-core` imports the native model directly and no longer depends on `codex-protocol`. Pressure derives from native `EpiphanyTokenUsageSnapshot`; Codex token telemetry is mapped at the bridge. Codex rollout/event reconstruction now lives in `codex-core::epiphany_rollout`, where the Codex `RolloutItem` / `EventMsg` types belong. Exact retrieval path matching is native to `epiphany-core`, so `epiphany-core` has no direct vendored Codex crate dependency.
- State-update contract proof and job launch/interrupt DTO ownership now live in native `epiphany-core`. The old giant `codex_thread.rs` Epiphany state-update test fixture was cut, and `epiphany-codex-bridge` no longer imports Epiphany job launch request types from `codex_core`; vendored `CodexThread` may call the native mutation/launch law for compatibility, but it must not regain ownership of state-update semantics, heartbeat launch-plan policy, or Epiphany DTOs.
- `epiphany-codex-bridge` no longer depends on vendored `codex-core` at all. Retrieval takes workspace/Codex-home paths, invalidation owns a native `notify` watcher, runtime-result acceptance takes typed state plus runtime-store path, live rollout/thread reads moved into app-server `epiphany_thread_host`, and mutation policy depends on the small `EpiphanyMutationHost` trait implemented by app-server's `EpiphanyCodexThreadHost` wrapper. The bridge owns Epiphany law; vendored app-server owns CodexThread access.
- `epiphany-codex-bridge` also no longer depends on `codex-protocol`. Bridge durable-state imports point at `epiphany-state-model`, token pressure takes native `EpiphanyTokenUsageSnapshot`, app-server maps Codex token telemetry at the host wall, Codex token usage rollout replay lives in `codex_message_processor/token_usage_replay.rs`, and bridge errors are `EpiphanyBridgeError`. The only remaining vendored bridge dependencies are app-server protocol DTOs and absolute path validation.
- The first bridge JSON-RPC starvation cuts are landed. Freshness now returns a serializable `EpiphanyFreshnessSurface`, job launch/interrupt mutation results carry typed `EpiphanyJobView` plus separate host-edge launcher/backend ids, reorient/pre-compaction decisions consume typed `EpiphanyPressure`, reorient policy consumes typed freshness directly, and reorient launch documents/results use typed core reorient decisions/status before app-server projects them into legacy JSON-RPC DTOs. Continue by cutting the full view/coordinator board into typed surfaces, not by inventing adapters with nicer uniforms.
- Coordinator/reorient decision/status is less contaminated now: `epiphany-codex-bridge` stores reorient decisions, coordinator decisions, source signals, CRRC recommendations, and role-board lanes as core-shaped types, core CRRC/coordinator/role-board surfaces serialize natively, and `ThreadEpiphany*` projection happens at the legacy view emission boundary. Keep cutting remaining result inputs until Aquarium/CultNet can read coordinator surfaces without JSON-RPC DTO authority.
- Core role/reorient finding interpretations and acceptance bundles now serialize natively too. Use those typed result surfaces as the next authority target; do not let `ThreadEpiphanyRoleFinding` or `ThreadEpiphanyReorientFinding` remain the bridge's internal result truth just because legacy view/accept responses still expose them.
- Runtime-result loading now has core typed snapshot APIs for role and reorient worker results, with legacy protocol tuple wrappers layered over them. This intentionally creates temporary duplicate note rendering; the next cut should move coordinator and acceptance consumers to typed snapshots, then delete protocol-shaped result rendering instead of preserving both.
- Coordinator result gating now consumes typed runtime result snapshots and core finding interpretations directly. `ThreadEpiphanyRoleFinding` / `ThreadEpiphanyReorientFinding` remain in legacy runtime wrapper/view/mutation response paths; the next cut is acceptance/mutation so protocol findings stop being the accepted-result authority.
- Acceptance/mutation now builds role/reorient receipts, evidence, observations, scratch, and checkpoint updates from core typed finding interpretations. Protocol findings remain as legacy response payloads and helper residue; delete those wrappers once view/result responses are also typed internally.
- The old protocol-shaped result helper layer is deleted: no protocol finding runtime-id helpers, no protocol result-note renderers, and no dead protocol reorient scratch/checkpoint builders remain in the bridge. Legacy view/result/accept responses may still carry `ThreadEpiphany*Finding`, but they are projections, not bridge policy input.
- Runtime result view responses now project directly from typed result snapshots; the old protocol tuple wrappers in `runtime_results.rs` are gone, and runtime-result tests assert core typed statuses.
- Role acceptance now applies core `EpiphanyStateUpdate` from typed findings; `ThreadEpiphanyUpdatePatch` survives in that path only as legacy accept-response projection.
- Role-board derivation consumes core `EpiphanyPressure` directly now; `ThreadEpiphanyPressure` is only response projection.
- Job reflection is now core-shaped inside the bridge. `map_epiphany_jobs` returns typed `EpiphanyJobView`, role-board/view/result assembly consumes that typed job surface, and `ThreadEpiphanyJob` is projected only when building legacy JSON-RPC response structs.
- Context and graph-query derivation now receive core `EpiphanyContextParams` / `EpiphanyGraphQuery` documents. The bridge converts `ThreadEpiphanyContextParams` / `ThreadEpiphanyGraphQuery` before invoking core policy, so Codex JSON-RPC request DTOs no longer define the internal graph question.
- Role/reorient acceptance service results now carry core typed findings, and role acceptance carries a typed `EpiphanyRoleStatePatchDocument` for the applied patch. App-server mutation routes project those into `ThreadEpiphany*Finding` / `ThreadEpiphanyUpdatePatch` only for legacy accept responses.
- Generic update/promote service entry points now accept typed `EpiphanyRoleStatePatchDocument` patches. Vendored app-server converts `ThreadEpiphanyUpdatePatch` request cargo before invoking the bridge, so the mutation service no longer treats the JSON-RPC patch as its native write document.
- Changed-field authority is native now. `EpiphanyStateUpdatedField` lives in `epiphany-core`; bridge mutation results carry that typed enum, while app-server projects it into `ThreadEpiphanyStateUpdatedField` only for JSON-RPC responses and notifications.
- Role launch, role accept, and role-result runtime lookup now use core `EpiphanyRoleResultRoleId` inside the bridge. App-server maps `ThreadEpiphanyRoleId` at the JSON-RPC boundary and keeps it only for legacy request/response shape.
- Reorient launch service results now use typed `EpiphanySurfaceSource`; app-server projects it into `ThreadEpiphanyReorientSource` only for the legacy launch response.
- Generic job launch now converts protocol `ThreadEpiphanyWorkerLaunchDocument` cargo at the app-server edge before building the core `EpiphanyJobLaunchRequest`; the bridge launch builder takes a typed `EpiphanyWorkerLaunchDocument`.
- Coordinator note rendering now consumes core CRRC/coordinator actions, pressure, role/reorient result statuses, and reorient state status. `ThreadEpiphany*` status/action DTOs are no longer the native shape for that prompt/text policy helper.
- Context and graph-query response builders now take core `EpiphanyContextParams` / `EpiphanyGraphQuery` documents. App-server read routes convert `ThreadEpiphanyContextParams` / `ThreadEpiphanyGraphQuery` at the JSON-RPC wall before invoking bridge derivation.
- Role-result response assembly now takes core `EpiphanyRoleResultRoleId`; `ThreadEpiphanyRoleId` is reconstructed only for the legacy response payload and finding projection.
- Distill response assembly now consumes core `EpiphanyDistillInput`; `ThreadEpiphanyDistillParams` is converted before bridge policy invocation.
- Role/reorient result response inputs now use core `EpiphanySurfaceSource`; `ThreadEpiphanyRolesSource` / `ThreadEpiphanyReorientSource` are response projection only.
- Changed-field derivation now inspects typed `EpiphanyRoleStatePatchDocument` directly. The bridge no longer converts typed patches back into `ThreadEpiphanyUpdatePatch` merely to calculate changed fields.
- View-lens orchestration now uses bridge-native `EpiphanyViewLens` selectors. App-server converts `ThreadEpiphanyViewLens` at the request wall, and the bridge maps back only for the legacy response echo.
- Coordinator status derivation now receives core `EpiphanyCrrcResultStatus` for reorient result state; `ThreadEpiphanyReorientResultStatus` is projection-only for coordinator/view responses.
- Coordinator view response assembly now receives core reorient state and role-board status; `ThreadEpiphanyReorientStateStatus` / `ThreadEpiphanyRoleLane` are projected inside the response builder.
- Coordinator judgment is no longer bridge-owned. `epiphany-core` now exposes `EpiphanyCoordinatorStatusInput` -> `EpiphanyCoordinatorStatus`; the bridge gathers runtime snapshots and accepted-finding facts, then core derives source signals and the coordinator decision. Reorient worker launch composition is also core-owned through `EpiphanyReorientLaunchRequestInput` -> `EpiphanyJobLaunchRequest`; bridge supplies the prompt instruction plus retained compatibility binding/owner constants and performs the host launch side effect.
- Coordinator finding signals are also core-owned. `derive_coordinator_finding_signals` derives modeling/verification/reorient accepted, reviewable, coverage, verdict, and evidence sequencing facts from typed findings plus `EpiphanyThreadState`; the bridge now loads snapshots/state and projects Codex JSON-RPC responses instead of keeping a duplicate acceptance tribunal.
- View lens selector semantics are core-owned now. `EpiphanyViewLens`, default view lenses, and lens dependency planning live in `epiphany-core`; `epiphany-codex-bridge::protocol_edge` converts Codex JSON-RPC lens params to core selectors and exposes the core dependency plan so app-server only decides which host facts to load.
- Correction: app-server must not own Epiphany request mapping. Codex JSON-RPC request DTO conversion now lives in `epiphany-codex-bridge::protocol_edge`; vendored app-server calls those edge adapters and no longer depends directly on `epiphany-core`.
- Context, graph-query, distill, generic job-launch document, and generic update/promote patch request cargo now enters bridge as core `EpiphanyContextParams`, `EpiphanyGraphQuery`, `EpiphanyDistillInput`, `EpiphanyWorkerLaunchDocument`, and `EpiphanyRoleStatePatchDocument`.
- Freshness/job/reorient response projection also belongs to the bridge edge now. Vendored app-server no longer keeps local helpers for `EpiphanyFreshnessSurface`, `EpiphanyJobView`, `EpiphanySurfaceSource`, or reorient decision/status enum projection; it gathers host facts, invokes bridge/core services, and sends the Codex JSON-RPC response.
- Job projection has one mapper now. `epiphany-codex-bridge::jobs` derives typed `EpiphanyJobView` and no longer imports app-server protocol DTOs; `protocol_edge::protocol_job_from_surface` is the only `EpiphanyJobView` -> `ThreadEpiphanyJob` conversion path.
- Runtime result loading is typed-only now. Role/reorient result status projection moved out of `runtime_results` and duplicate coordinator helpers into `protocol_edge`, so view and coordinator response builders share one Codex JSON-RPC status mapper.
- Pressure projection is centralized too. `pressure.rs` derives typed `EpiphanyPressure`, coordinator consumes typed pressure signals, and `protocol_edge` owns the `ThreadEpiphanyPressure*` DTO mapping.
- Reorientation derivation is typed-only too. `reorient.rs` no longer imports app-server protocol DTOs and no longer exposes the unused freshness DTO tuple; view/coordinator response builders call `protocol_edge` for reorient state/decision projection.
- Result projection moved to the edge. `results.rs` now renders typed result notes only; `protocol_edge` owns role-id request conversion plus role/reorient finding and state-patch projection for Codex JSON-RPC responses.
- State-updated notification projection moved to the edge as well. `mutation.rs` now owns typed mutation/acceptance mechanics without importing app-server protocol DTOs.
- `pressure.rs` is typed-only now. The convenience helper returning `ThreadEpiphanyPressure` was deleted; view response assembly derives typed pressure and calls `protocol_edge` for projection.
- Coordinator projection is split out too. `coordinator.rs` no longer imports app-server protocol DTOs; it owns typed coordinator status, CRRC input shaping, role-board derivation, automation verdict selection, and note rendering. `coordinator_protocol.rs` owns Codex JSON-RPC projection for coordinator, CRRC, and role-board response payloads.
- Context/planning/graph projection is split out too. `context.rs` returns typed `epiphany-core` view documents; `context_protocol.rs` owns Codex JSON-RPC tuple/projection cargo for context, planning, and graph-query responses.
- Scene projection is split out too. `scene.rs` returns the typed core `EpiphanyScene`; `scene_protocol.rs` owns `ThreadEpiphanyScene` and scene-action response projection.
- Retrieval projection is split out too. `retrieve.rs` normalizes, indexes, looks up state, and retrieves typed core results; `retrieve_protocol.rs` owns Codex JSON-RPC retrieve/index response projection and absolute-path DTO validation.
- View response assembly no longer owns private lens/source/reorient-status projection helpers; those enum conversions route through `protocol_edge`. The Codex response assembler is now honestly named `view_protocol.rs`. The larger typed-view wall is still the next architectural judgment, not something to fake with another adapter.
- MCP remains JSON because MCP is a JSON protocol. The latest processor cut only moved MCP refresh/OAuth/status/resource/tool-call route handling into `codex_message_processor/mcp_routes.rs`, reducing the root processor to about 8,977 lines; it did not claim MCP payloads are CultNet or make MCP an Epiphany-owned runtime contract.
- Codex account auth route handling remains in vendored Codex but now lives in `codex_message_processor/auth_routes.rs`: API-key login, ChatGPT browser/device-code/external-token login, logout, auth status, account, rate limits, and add-credits nudge handling. This preserves Codex ownership of subscription auth while cutting the root processor to about 8,094 lines.
- One-off command execution and command stream write/resize/terminate route handling now live in `codex_message_processor/command_routes.rs`; `CommandExecManager` remains the lifecycle owner, while the root processor is about 7,800 lines.
@ -59,7 +113,7 @@ remembers doctrine; the branch remembers the blade.
- Thread start route handling and its async lifecycle task now live in `codex_message_processor/thread_start_routes.rs`; root no longer owns `thread_start_task`.
- Thread config helpers now live in `codex_message_processor/thread_config.rs`: instruction-source loading, thread config override construction, config-load error shaping, and dynamic-tool validation. The pure dynamic-tool validation contract has module-local unit tests.
- Turn control routes now live in `codex_message_processor/turn_routes.rs`: turn start, thread item injection, app-server client info setting, turn steer, turn interrupt, mode normalization, and input-limit validation with module-local unit tests.
- Heartbeat durable document/schema types now live in `epiphany-core/src/heartbeat_state/heartbeat_documents.rs`, role/default construction lives in `epiphany-core/src/heartbeat_state/heartbeat_roles.rs`, persistence/validation lives in `epiphany-core/src/heartbeat_state/heartbeat_store.rs`, and status/artifact JSON projection lives in `epiphany-core/src/heartbeat_state/heartbeat_projection.rs`. The root `heartbeat_state.rs` keeps scheduling and Void cognition orchestration for the next cuts. Focused heartbeat tests and `epiphany-core` check pass after the splits.
- Heartbeat durable document/schema types now live in `epiphany-core/src/heartbeat_state/heartbeat_documents.rs`, role/default construction lives in `epiphany-core/src/heartbeat_state/heartbeat_roles.rs`, persistence/validation lives in `epiphany-core/src/heartbeat_state/heartbeat_store.rs`, and status/artifact JSON projection lives in `epiphany-core/src/heartbeat_state/heartbeat_projection.rs`. Adaptive pacing, scene protocol, participant personality/mood timing, initiative heat projection, birth personality seed receipts, pending-turn freeze/multiplier facts, Void routine sleep-cycle physiology, Void routine memory-resonance pairs, incubation themes/source coverage, analytic/associative thought lanes, bridge syntheses/saturation/refractory cooling/decision, candidate interventions, role thought appraisals, and derived reactions are typed fields rather than `extra` maps or generic JSON cargo; only the sealed legacy combined-state reader still carries `extra` for old `voidRoutine` migration. The root `heartbeat_state.rs` keeps scheduling and cognition orchestration; do not reintroduce generic JSON there merely because operator artifacts are JSON.
- Heartbeat role/default and store seams now have module-local unit tests: fixed lane catalog, Ghostlight scene role projection, and typed state/cognition CultCache round-trip. Keep future heartbeat cuts aligned to those seam tests instead of growing root integration-only coverage.
- Heartbeat adaptive pacing now lives in `epiphany-core/src/heartbeat_state/heartbeat_pacing.rs`, including running-turn count and effective cooldown multipliers. It has a module-local unit test for pending-turn counting; root scheduling calls the pacing seam instead of owning pressure/concurrency math directly.
- Thread projection now lives in `codex_message_processor/thread_projection.rs`: state-db/thread-store/rollout summary projection, `ConversationSummary` to API `Thread` mapping, thread-title attachment helpers, turn pagination, and rollout turn reconstruction. `codex_message_processor.rs` is about 1,009 lines after this extraction and mostly owns JSON-RPC dispatch, processor construction, shared thread lookup/submit/error helpers, and lifecycle shutdown.
@ -92,20 +146,25 @@ remembers doctrine; the branch remembers the blade.
- The prompt doctrine pass is landed. Shared Epiphany prompts now carry distilled memory/evidence discipline. Rendered state intro/doctrine text lives in `epiphany-core/src/prompts/`, and lane/control prompt text lives in `vendor/codex/codex-rs/app-server/src/prompts/epiphany_specialists.toml`: modeling is the Body, implementation is the Hands and GUI-launched main coding lane, verification is the Soul, reorientation is Life, coordinator remains the read-only Self, and CRRC owns the pre-compaction intervention template.
- The machine-priest voice now says the quiet part plainly: crusade/heresy/purity language is aimed at technical rot, hidden state, duplicate truth, drift, and lying structures, not at humans. Future Epiphany swarms should inherit severity toward systems and curiosity toward people.
- The Ghostlight memory pass is landed. `epiphany_specialists.toml` now has a shared persistent-memory projection prepended by the harness to fixed role specialists, reorientation workers, coordinator notes, and CRRC checkpoint interventions. The rendered base doctrine also states the Perfect Machine rule directly: prompt is projection, durable typed state is the mind, every lane must improve its own memory/model/prompt/evidence habit or name the repair, and each lane phrases that duty in its own organ language so the salience sticks.
- The role self-memory persistence pass is native now. Each lane has a Ghostlight-shaped typed dossier in `state/agents.msgpack`, and specialists may return optional `selfPatch` requests beside their normal role result. `roleResult`/`roleAccept` project coordinator review as `selfPersistence`: accepted requests are role-matched, bounded lane memory/goal/value/private-note mutations; refused requests explain wrong role, project-truth smuggling, authority grabs, bloat, missing reason, or malformed records. GUI/coordinator accept paths apply accepted `selfPatch` requests through the native `epiphany-agent-memory-store` binary; project truth still belongs only in `EpiphanyThreadState`.
- The heartbeat initiative pass is landed as a bounded tool seam. `state/agent-heartbeats.msgpack` tracks Self, Face, Imagination, Eyes, Body, Hands, Soul, and Life as Ghostlight-style initiative participants with arena, participant kind, speed, next-ready time, reaction bias, interrupt threshold, load, status, constraints, personality cooldown, mood cooldown, effective cooldown, adaptive pacing, history, and pending turns through `epiphany-core::EpiphanyHeartbeatStateEntry` and the native `epiphany-heartbeat-store` binary. Role dossiers set baseline timing; appraisal mood/anxiety bends it through urgency, arousal, thought pressure, guardedness, and reaction intensity, so Hands-like work pressure and high-need anxious lanes recover sooner without being heartbeaten while a prior turn is running. `epiphany-heartbeat-store pump` computes pressure from external urgency, mood/anxiety, reaction intensity, thought pressure, and current pending load, then chooses tempo plus target concurrency: calm can launch zero and sleep slow; alarm can fill most active lanes. Idle turns are for rumination: light thought shuffling, role-quality attention, and candidate selfPatch pressure. Sleep/dream cycle passes are the intended distillation window for durable self-memory and doctrine. JSON heartbeat state and Python wrapper state are gone; general CultCache schema sync, polyglot domain loading, and debug display tools belong in CultLib. This is a callable scheduler seam, not yet an always-on daemon; a heart valve, not a whole circulatory god.
- The role self-memory persistence pass is native now. Each lane has a Ghostlight-shaped typed dossier in `state/agents.msgpack`, and specialists may return optional `selfPatch` requests beside their normal role result. Relationship, event, scene, and perceived-overlay arrays are typed dossier documents, not JSON cargo, and the durable structs do not keep unknown `extra` maps. `roleResult`/`roleAccept` project coordinator review as `selfPersistence`: accepted requests are role-matched, bounded lane memory/goal/value/private-note mutations; refused requests explain wrong role, project-truth smuggling, authority grabs, bloat, missing reason, or malformed records. GUI/coordinator accept paths apply accepted `selfPatch` requests through the native `epiphany-agent-memory-store` binary; project truth still belongs only in `EpiphanyThreadState`.
- The heartbeat initiative pass is landed as a bounded tool seam. `state/agent-heartbeats.msgpack` tracks Self, Face, Imagination, Eyes, Body, Hands, Soul, and Life as Ghostlight-style initiative participants with arena, participant kind, speed, next-ready time, reaction bias, interrupt threshold, load, status, constraints, personality cooldown, mood cooldown, effective cooldown, adaptive pacing, history, and pending turns through `epiphany-core::EpiphanyHeartbeatStateEntry` and the native `epiphany-heartbeat-store` binary. Role dossiers set baseline timing; appraisal mood/anxiety bends it through urgency, arousal, thought pressure, guardedness, and reaction intensity, so Hands-like work pressure and high-need anxious lanes recover sooner without being heartbeaten while a prior turn is running. Mood is now official physiology, not prose: `HeartbeatMoodTiming.emotional_dimensions` carries the 32-axis current affect vector that utterance state turns into the Weksa/AquaSynth 64-float character-state lane. `epiphany-heartbeat-store pump` computes pressure from external urgency, mood/anxiety, reaction intensity, thought pressure, and current pending load, then chooses tempo plus target concurrency: calm can launch zero and sleep slow; alarm can fill most active lanes. Idle turns are for rumination: light thought shuffling, role-quality attention, and candidate selfPatch pressure. Sleep/dream cycle passes are the intended distillation window for durable self-memory and doctrine. JSON heartbeat state, Python wrapper state, live heartbeat timing `extra` maps, JSON `sleepCycle` persistence, and JSON `memoryResonance` persistence are gone; general CultCache schema sync, polyglot domain loading, and debug display tools belong in CultLib. This is a callable scheduler seam, not yet an always-on daemon; a heart valve, not a whole circulatory god.
- Heartbeat initiative heat is now part of that seam. `EpiphanyHeartbeatStateEntry` owns `initiative_heat`, pacing applies active global/all/agent/role/arena/participant-kind/group/constraint multipliers into per-participant `initiativeHeatMultiplier`, and recovery divides by that multiplier. The CLI exposes `epiphany-heartbeat-store heat`, status/schedule projections show heat, and the CultNet catalog advertises both `epiphany.heartbeat_initiative_heat.v0` and coordinator intent `epiphany.heartbeat_heat_intent.v0`; runtime-spine hello lists the heat intent on the heartbeat mutation contract. Smoke proves a heated implementation lane reaches the live action catalog with `initiative_heat_multiplier: 4.0`.
- Active heartbeat turns now freeze initiative explicitly. A running pending turn carries `initiativeFrozen` and `initiativeFreezeReason`; schedule/status/readiness projections expose `initiative_frozen`; the scheduler still rejects attempts to wake that lane again; and completion receipts mark `cooldownStartedAfterCompletion: true`. A focused high-heat test proves a 25x heated implementation lane cannot be queued again while it is still thinking.
- The first Ghostlight-derived timing slice is landed in Epiphany, but Ghostlight is reference lineage rather than a sibling runtime to preserve. `epiphany-heartbeat-store init --profile ghostlight-scene --scene-id <id> --scene-participant <id|name|speed|reaction|threshold|constraints>` creates a typed CultCache MessagePack scene heartbeat store whose participants are `arena=scene`, `participantKind=character`; `tick` emits `ghostlight.initiative_schedule.v0` receipts with `scene_turn` actions and local-affordance basis. The generic Epiphany maintenance lanes are not auto-patched into scene stores.
- The first Void-derived routine slice is landed in Epiphany, but VoidBot is reference lineage rather than a runtime dependency. `epiphany-heartbeat-store routine --store .\state\agent-heartbeats.msgpack --artifact-dir .\.epiphany-heartbeats --agent-store .\state\agents.msgpack` reads typed role dossiers, computes bounded memory resonance, maintains incubation themes, runs analytic and associative cognition lanes, writes bridge syntheses/saturation/tension, projects the active thought cluster through each role's Ghostlight-shaped personality vectors, derives participant-local reactions, applies personality/mood timing, advances the sleep/dream cycle, updates the typed heartbeat store, and emits an auditable `epiphany.void_routine.v0` receipt. The routine now also carries anti-loop physiology imported from Void's calmer brain: `noveltyToSelf`, `noveltyToRoom`, source coverage, saturation pressure, refractory cooling, and explicit permission to let a live unsaturated thought deepen without forcing novelty theater every pass. This mutates only heartbeat physiology fields: project truth and role memory mutation remain on their reviewed surfaces.
- The first Void-derived routine slice is landed in Epiphany, but VoidBot is reference lineage rather than a runtime dependency. `epiphany-heartbeat-store routine --store .\state\agent-heartbeats.msgpack --artifact-dir .\.epiphany-heartbeats --agent-store .\state\agents.msgpack` reads typed role dossiers, computes typed bounded memory resonance, maintains typed incubation themes/source coverage, runs typed analytic and associative cognition lanes, writes typed bridge syntheses/saturation/refractory cooling/decision state, produces typed candidate interventions, projects the active thought cluster through each role's Ghostlight-shaped personality vectors, derives typed participant-local reactions from typed appraisals, applies personality/mood timing, advances typed sleep/dream-cycle state, updates the typed heartbeat store, and emits an auditable `epiphany.void_routine.v0` receipt. The routine now also carries anti-loop physiology imported from Void's calmer brain: `noveltyToSelf`, `noveltyToRoom`, source coverage, saturation pressure, refractory cooling, and explicit permission to let a live unsaturated thought deepen without forcing novelty theater every pass. This mutates only heartbeat physiology fields: project truth and role memory mutation remain on their reviewed surfaces.
- The next memory foundation must not build separate engines for repo graph and agent mind. Heartbeat cognition, role self-memory, repo architecture/dataflow, evidence scars, incubation, agency pressure, and context packets should converge through `EpiphanyMemoryGraph` profiles so one typed graph owns anchors, lifecycle, summaries, freshness, context cuts, and Qdrant embedding manifests. Repo, agent, and heartbeat profile producers now map accepted graph truth, reviewed role dossier memories/goals/values, and provisional heartbeat short-term/incubation/agency/candidate pressure into memory graph documents without scanner/Qdrant/model work. Heartbeat import deliberately does not mark durable promotion. Compose now merges profile snapshots into one validated graph and rejects duplicate authority; `epiphany-memory-graph compose` writes a composed typed CultCache graph from typed source graph stores; `epiphany-memory-graph refresh` can invoke live agent-memory plus heartbeat-cognition producers from their typed stores into one graph store; `epiphany-core` has a native typed `EpiphanyThreadState` CultCache store seam; the bridge mirrors loaded Codex-thread Epiphany state into `state/thread-state.msgpack`; and CultNet now advertises logical `EpiphanyThreadState` plus `EpiphanyMemoryGraph` snapshot contracts. The next clean slice is local refresh/dogfood once a mirrored thread-state store exists, then a Qdrant embedding cache writer that treats embeddings as rebuildable manifests, not canonical memory.
- Manual Codex-run rumination still has an explicit aftercare rule until Epiphany owns the full sleep consolidator. In the intended Epiphany cycle, sub-agents ruminate when idle and distill when they sleep; in this supervising Codex thread, a heartbeat/routine vigil is only physiology until a separate closing pass reviews the receipts and decides whether map, handoff, evidence, or role self-memory should change.
- The Face public-surface pass is landed as a bounded lane. Face's role dossier lives in `state/agents.msgpack`; `epiphany_specialists.toml` gives it a VoidBot-heartbeat-derived prompt stripped of moderation authority; `state/face-discord.toml` and the native `epiphany-face-discord` binary enforce that Face may interact only through #aquarium. Missing channel id or token writes candidate chat artifacts instead of posting elsewhere. If a Face has a persona name/avatar, `post` uses the shared Discord webhook pipe so each Epiphany can speak with its own nickname and avatar without a separate bot identity.
- The Void memory bridge is now native enough to keep the useful organs attached. `state/void-memory.toml` and `epiphany-void-memory` can check Void's Docker Postgres state spine, query Qdrant Discord-history and source/lore collections with the configured Ollama embedding model, and fetch raw archive context windows from Void's file-backed Discord archive. Native `epiphany-mvp-status` includes a `voidMemory` block so Aquarium/backend clients can inspect whether the mouth and memory are actually wired.
- The first Epiphany-native character-loop pass is landed. `epiphany-character-loop turn --role face --stimulus <text>` loads Face's typed dossier from `state/agents.msgpack` and emits an auditable `epiphany.character_turn_packet.v0` packet with projected local context, visible stimulus, allowed outputs, and guardrails. The same packet builder works for other role ids, because every organ already has a Ghostlight-shaped dossier.
- The Epiphany-native character-loop now emits a Weksa/AquaSynth-ready utterance-state seam. `epiphany-character-loop turn --role face --stimulus <text>` loads Face's typed dossier from `state/agents.msgpack`, builds typed projection/appraisal/reaction seeds from the shared heartbeat cognition documents, and emits an auditable `epiphany.character_turn_packet.v0` packet with `epiphany.agent_utterance_state.v0`. That utterance state is memory-free: identity, personality vectors, values, current mood, activation, 32 named affect dimensions, and the fixed 64-float `weksa.utterance_embedding_handoff.v0.1` character-state vector for AquaSynth/Weksa speech embedding. The same packet builder works for other role ids, because every organ already has a Ghostlight-shaped dossier.
- The heartbeat/Face operator API pass is landed. Native `epiphany-heartbeat-store status` returns machine-readable initiative state plus CultCache store presence, thought appraisals, and derived reactions; native `epiphany-face-discord bubble` writes Discord-independent `epiphany.face_bubble.v0` artifacts, and native `epiphany-mvp-status` includes `heartbeat` and `face` blocks. Aquarium should call native/backend surfaces rather than resurrecting deleted Python action shims.
- The native runtime-spine pass is landed as a first vertebra, not a full daemon. `state/runtime-spine.msgpack` is the default store, `.epiphany-dogfood/runtime-spine-job/runtime.msgpack` and `hello.cultnet` are the latest job/result smoke artifacts, and specialist launch/result flows now route through typed heartbeat/runtime-spine documents. Its CultNet hello advertises mutation contracts for runtime/session/job/memory/heartbeat/ledger documents, with coordinator-owned intent/receipt paths for writes.
- The Aetheria dogfood run has a contamination scar. The supervising Codex session directly edited and committed target-repo work on `E:\Projects\Aetheria-Economy` instead of only driving Epiphany lanes. Treat those Aetheria commits as supervisor-seeded implementation, not clean evidence that Epiphany coordinated the work. Future dogfood must run through the GUI/coordinator/fixed role lanes with auditable artifacts unless the user explicitly authorizes an operator intervention. Remember the sunburn: do not stare into the worker's objective until you become it.
- The dogfood quarantine now has a direct-thought boundary. The supervisor may read coordinator actions, role/reorient statuses, structured finding summaries, reviewed state patches, rendered status snapshots, and artifact manifests. It must not read raw worker transcripts, direct worker messages, full turn logs, or `rawResult` payloads during normal dogfood. Those artifacts are sealed black reliquaries for explicit forensic debugging only.
- Native `epiphany-agent-telemetry` is the safe instrument panel for sealed runs. Status/coordinator/GUI/dogfood/live-specialist tools generate telemetry JSON from sealed transcripts that preserves method names, call shape, job/status/path counts, and any visible function/tool names while sealing text, direct messages, and raw results.
- Epiphany now has a VoidBot repo Face identity registered from `E:\Projects\VoidBot\.voidbot\private\repo-discord-identities.json` with local ignored state under `.voidbot/`: `voice/identity.json`, `state/epiphany.cc`, and birth artifacts/logs. The newborn Face state has distilled Epiphany doctrine as typed long-term identity memories and values: cute pushy machine-saint persona, architectural purity, CultCache/CultNet first, xenos-language safety boundary, relentless work/caretaking needs, Codex-auth reliquary, map/scratch/evidence discipline, unified memory graph, dogfood supervision boundary, review-gated agency, heartbeat physiology, and interview-grade modularity.
- Latest Aetheria supervised dogfood thread: `019ddc52-4ee8-7203-b6c0-106a9c270067` with codex-home `.epiphany-dogfood/aetheria-supervised/codex-home`. It has now exercised modeling, verification, implementation audit, no-diff repair, CRRC reorientation, reorient acceptance, and coordinator replay through operator-safe artifacts without opening sealed worker thoughts.
- Coordinator policy now treats accepted verifier results as implementation clearance only when the verifier verdict is `pass`. Accepting a `needs-evidence`, `needs-review`, or `fail` verifier finding records the review but routes the loop back toward modeling/checkpoint strengthening or reorientation. The policy was tightened after a too-permissive coordinator path treated reviewed verifier output as a green light.
- A later dogfood attempt exposed another supervision scar: manually launching modeling, reorientation, and verification from the supervisor is not clean dogfood. The coordinator now treats `regatherManually` as fallback only after fixed lanes cannot advance, stops at `reviewModelingResult` for completed unaccepted modeling findings, and the CLI runner's explicit test-only `--auto-review` mode can accept modeling `statePatch` findings before launching verification. Production semantic findings remain review-gated.
@ -192,8 +251,8 @@ The current spine, blessed but not yet finished:
- distilled shared and config-backed prompt doctrine through the base prompt, rendered Epiphany state prompt files, modeling/Body, implementation/Hands, verification/Soul, reorientation/Life, coordinator/Self, and CRRC intervention surfaces
- shared Ghostlight-derived persistent memory projection across Imagination, Body, Soul, Life, Self, CRRC checkpoint steering, and the GUI-launched Hands implementation lane
- Ghostlight-derived heartbeat initiative scheduling through the native `epiphany-heartbeat-store` binary, with Self and Face included as first-class Epiphany maintenance participants, Ghostlight scene characters supported through the same timing law, role-personality cooldowns, appraisal mood/anxiety cooldowns, effective recovery timing, adaptive pressure-based pump tempo/concurrency, idle rumination feeding candidate self-memory pressure, sleep/dream passes serving as the distillation window, and heartbeat state persisted through typed CultCache MessagePack stores
- Void-derived native routine physiology through `epiphany-heartbeat-store routine`, with sleep cycle, memory resonance, incubation, analytic/associative cognition lanes, bridge judgment, candidate interventions, Ghostlight-style personality appraisals, mood/anxiety timing, derived reactions, and dream maintenance stored in the typed heartbeat store
- Epiphany-native character-loop packet projection through `epiphany-character-loop`, with Face as the first public-surface actor over its typed role dossier
- Void-derived native routine physiology through `epiphany-heartbeat-store routine`, with typed sleep cycle, typed memory resonance, typed incubation, typed analytic/associative cognition lanes, typed bridge judgment/cooling, typed candidate interventions, typed Ghostlight-style personality appraisals, mood/anxiety timing, typed derived reactions, and dream maintenance stored in the typed heartbeat store
- Epiphany-native character-loop packet projection through `epiphany-character-loop`, with Face as the first public-surface actor over its typed role dossier and typed heartbeat-derived projection/appraisal/reaction seeds before JSON artifact serialization
- Face as the public #aquarium-only surface for translating agent thought-weather into short chats or candidate drafts, with persona webhook presentation and Void memory/search access, without moderator authority
- Ghostlight-shaped role dossiers in `state/agents.msgpack`, plus `selfPatch` review projection through `roleResult`/`roleAccept` and accepted memory application through the native `epiphany-agent-memory-store` binary
- first Unity runtime bridge through native `epiphany-unity-bridge`, native `epiphany-unity-bridge-smoke`, GUI Inspect Unity, runtime artifact listing, and implementation prompt guardrails
@ -652,21 +711,20 @@ there is one runtime job for the worker, owned by runtime-spine, and the worker
runner completes that job.
The Codex-core re-export husks for `epiphany_distillation`, `epiphany_promotion`,
`epiphany_proposal`, and `epiphany_retrieval` have also been deleted.
`codex-core` now re-exports those native types/functions directly from
`epiphany-core`, and `CodexThread` calls `epiphany_core::retrieve_workspace` /
`index_workspace` / `retrieval_state_for_workspace` directly.
The `epiphany_rollout` husk is gone too; `codex-core::lib` keeps only the
one host-boundary function that passes Codex's turn-boundary predicate into
`epiphany-core`.
`epiphany_proposal`, and `epiphany_retrieval` have also been deleted. The
`epiphany_rollout` husk is gone too; `codex-core::lib` keeps only the one
host-boundary function that passes Codex's turn-boundary predicate into
`epiphany-core`. Vendored `codex-core` and `codex-app-server` no longer depend
directly on `epiphany-core`: `CodexThread` exposes host persistence/path facts,
while `epiphany-codex-bridge` calls native retrieval/indexing, state-update
policy, and launch request construction.
The runtime-spine job-opening mechanism for heartbeat/specialist launches has
also been pulled into `epiphany-core` as `open_runtime_spine_heartbeat_job`.
Vendored `codex_core::CodexThread::epiphany_launch_job` still validates,
persists, and updates Codex thread state, but it no longer owns the
initialize-session-open-job sequence. That is a small cut, but a real ownership
move: native runtime lifecycle belongs to the runtime spine, not the Codex
thread wrapper.
Launch/interrupt policy now lives in `epiphany-codex-bridge`; vendored
`CodexThread` only persists the typed state snapshot and exposes the runtime
store path. Native runtime lifecycle belongs to the runtime spine, not the
Codex thread wrapper.
That job-opening path now preserves the work order instead of shaving it into a
generic job id. `epiphany-core::EpiphanyRuntimeWorkerLaunchRequest` is a
@ -1227,7 +1285,7 @@ The prompt doctrine pass has now also run. It rechecked global AGENTS,
available Codex memories, and nearby evidence ledgers before distilling the
sane parts into the shared base prompt, rendered Epiphany state, and fixed lane
templates. Major prompt text now lives in prompt files instead of Rust/Python
string slabs: rendered state intro/doctrine lives under `epiphany-core/src/prompts/`,
string slabs: rendered state intro/doctrine lives under `epiphany-state-model/src/prompts/`,
and lane/control templates live in
`vendor/codex/codex-rs/app-server/src/prompts/epiphany_specialists.toml`.
Implementation is not a `roleLaunch` specialist; it is the GUI-launched main
@ -1303,6 +1361,69 @@ native `epiphany-phase6-graph-query-smoke` / native `epiphany-phase6-planning-sm
smokes are now guardrails, not the next organs. The architectural teardown says
the next organ is control-plane purification.
The public product direction is now explicit: Epiphany is project-native agency,
not merely a better prompt wrapper. VoidBot's repo-Face pattern is the live
small reference: Nibu/Aqua/Mimir have repo jurisdiction, Discord roles,
repo-local Face state, pending mentions, proposal authority, heartbeat
initiative, heat, and active-turn freeze. Epiphany should make this native and
larger: users talk to projects and their Faces through Aquarium, Discord,
voice/WebRTC, stream overlays, native CLIs, or other CultNet clients; the
project schedules modeling, research, verification, rumination, and proposed
actions without requiring the human to hand over a complete architecture brief.
The important Face lesson is now sharper than "better public speech." Face is
special because it needs to think narratively: Imagination acts as Projector,
turning typed state, repo-body activity, social pressure, organ dependencies,
and pending mentions into lived context before Face thinks; Face writes natural
prose only; Mind acts as Interpreter, translating that natural thought into
reviewed durable memory, draft, public SAY, route, or silence.
Epiphany has the first native slice of that membrane in `epiphany-core`:
`face_turn.rs` defines Imagination-Projector/Face/Mind-Interpreter prompt
contracts, heartbeat state carries typed pending mentions, and pending Face
mentions select a `face_turn` without letting Face side effects execute
themselves.
`notes/organ-dependency-contracts.md` is the standing dependency map: every
sub-agent depends on all the other standing organs. Dependency does not collapse
ownership; it makes each turn whole-organism-aware while Body still gates repo
access and Mind still gates durable state.
Mind is the larger version of the same boundary. The state is the Mind:
sub-agent outputs are thoughts, not authority. Role/reorient findings and their
`statePatch` / `selfPatch` / evidence / observation / receipt / scratch /
checkpoint effects must route through Mind review before they become durable
state. The first native `mind_gateway.rs` slice reviews role and reorientation
acceptance effects, and `epiphany-codex-bridge::mutation` now requires that
review before building state updates.
`notes/mind-cultnet-contracts.md` is the new contract map for making Mind the
persistent state guardian over CultNet/CultMesh. The code surface now names
`epiphany.mind.thought`, `epiphany.mind.state_effect_proposal`,
`epiphany.mind.gateway_review`, `epiphany.mind.state_commit_receipt`,
`epiphany.mind.state_rejection_receipt`, and
`epiphany.mind.verse_adoption_receipt`; runtime-spine advertises those
contracts, and CultMesh stores Verse-scoped `EpiphanyCultMeshMindContractEntry`
policy docs so `epiphany-internal` owns private state flow while
`epiphany-global` remains thought weather plus adoption receipts.
`notes/body-cultnet-contracts.md` is the matching substrate gate: Body owns repo
access. The code surface now names `epiphany.body.repo_access_request`,
`epiphany.body.repo_access_review`, grant/refusal receipts, snapshot receipts,
and mutation receipts; runtime-spine advertises those contracts, and CultMesh
stores Verse-scoped `EpiphanyCultMeshBodyContractEntry` policy docs. Hands, Eyes,
Face, workers, and bridge tools should touch the repo only through scoped Body
access receipts; Mind remains the separate durable-state gate after the touch.
`notes/eyes-cultnet-contracts.md` is the evidence gate: Eyes owns citable source
grounding. Body can grant access to the repo, but Eyes decides what was actually
looked at and emits evidence reviews, source lookup receipts, evidence packets,
or refusals before Imagination, Hands, Mind, Face, Soul, Self, Body, or Life cite
the material as known.
`notes/perfect-machine-audit-roadmap.md` is the current audit/path document. It
compares Epiphany's named organ contracts against Void's current Face prompting
shape and maps the route from contract catalog to executable organism. Main
finding: the boundaries are directionally right, but Mind/Body/Eyes are still
mostly advertised gates rather than mandatory runtime receipt chains; Hands,
Soul, and Life need their own contract families; and cargo verification is
blocked until the missing `E:\Projects\CultLib\crates\*` Rust dependency body is
repaired or deliberately replaced.
## Not Yet
- automatic watcher-driven semantic invalidation
@ -1314,6 +1435,7 @@ the next organ is control-plane purification.
- broader editor/runtime bridges beyond the first pinned Unity bridge
- broad event stream beyond the landed state update notification
- typed repetitive-work queues plus final-answer gates, so batch/tile/import/migration work cannot end merely because the pattern was demonstrated or the partial result sounds tidy
- public CultMesh Verse deployment or cross-app live sharing; only the local Rust CultMesh node/status smoke exists so far
The machine is no longer cleared to move outward. Free it from the Codex organ
first.

View file

@ -0,0 +1,68 @@
# Mind CultNet Contracts
Objective: make Mind the persistent state guardian. The state is the Mind; every
sub-agent output is a thought or proposal until Mind admits it into durable
state.
Body is the neighboring gate. Body decides whether an organ may touch the repo;
Mind decides whether the resulting thought, evidence, or proposal mutates
persistent state. Do not collapse these into one throne. That is how the machine
starts lying with excellent posture.
Imagination is the neighboring Projector for Face. It may compose a lived scene
from state, Body facts, social pressure, and organ dependencies, but it does not
interpret side effects or admit state.
## Authority Map
- Owner: Mind owns the decision to mutate persistent Epiphany state.
- Inputs: typed worker results, Face Interpreter intents, current state context,
runtime/job provenance, Verse ingress receipts, operator-approved intents, and
relevant verification evidence.
- Outputs: Mind gateway reviews, state commit receipts, state rejection
receipts, and Verse adoption receipts.
- Derived state: role findings, reorient findings, Face side-effect intents,
public dreams, local-area findings, `statePatch`, `selfPatch`, evidence,
scratch, checkpoints, objectives, and graph edits are proposal-only until Mind
accepts them.
- Forbidden writers: role acceptance, reorient acceptance, Face Interpreter,
public Verse ingress, local-area Verse ingress, raw worker result ingestion,
runtime job completion, and compatibility JSON-RPC routes must not directly
decide durable state mutation.
- Shared path: every durable state effect goes through a Mind state-effect
proposal and receives a Mind review plus commit or rejection receipt.
- Deletion line: old contract language that said "coordinator accepts memory
mutations" is demoted. Coordinator may schedule, route, and carry intent, but
Mind owns persistent-state admission.
## Verse Boundary
CultMesh simplifies the boundary by making Verse placement part of the contract:
- `epiphany-internal`: private thoughts, state-effect proposals, Mind reviews,
commit receipts, rejection receipts, runtime facts, heartbeat state, role
dossiers, and other private organ state.
- `gamecult-local`: trusted GameCult operator-safe sharing, non-secret status,
reviewable findings, and receipts. It does not carry raw worker thought,
private memory, or direct state mutation authority.
- `epiphany-global`: public thought weather: dreams, questions, hypotheses,
Face posts, and adoption receipts. Nothing from this Verse mutates local
memory, planning, doctrine, governance, or project truth without a reviewed
local adoption receipt.
## Contract Families
- `epiphany.mind.thought`: sub-agent output submitted as thought, not durable
state authority.
- `epiphany.mind.state_effect_proposal`: proposed durable mutation. Mind
accepts, refuses, or holds it.
- `epiphany.mind.gateway_review`: durable receipt explaining the Mind route.
- `epiphany.mind.state_commit_receipt`: proof that Mind admitted an effect into
persistent state.
- `epiphany.mind.state_rejection_receipt`: proof that Mind refused or held an
effect without mutating state.
- `epiphany.mind.verse_adoption_receipt`: local adoption receipt for public or
foreign Verse material.
The current native surfaces are in `epiphany-core::mind_gateway` and the
Verse-scoped policy documents are in `epiphany-core::cultmesh_integration`.

View file

@ -0,0 +1,57 @@
# Organ Dependency Contracts
Objective: every sub-agent depends on all the other sub-agents. No organ is a
little sovereign state. Each one owns a boundary, and each one must feel the
pressure of the others before acting.
## Standing Organs
- `self`: routing, authority boundaries, and coordination.
- `face`: public presence and person-shaped conversation.
- `imagination`: projection, possible worlds, plans, and narrative scene
construction.
- `eyes`: evidence seeking and source inspection.
- `body`: repository/substrate access and anatomy.
- `hands`: edits, commands, and external action.
- `soul`: verification, ethics, invariants, and falsification.
- `life`: continuity, compaction, sleep, handoff, and recovery.
## Dependency Rule
For every standing organ `X`, `X.dependsOn` is every other standing organ.
That does not mean every organ owns every decision. It means every organ's turn
must be projected with awareness of the whole organism:
- Face speaks with Body facts, Mind-reviewed memory, Soul constraints, Life
continuity, Self routing pressure, Eyes uncertainty, Hands consequence, and
Imagination scene.
- Hands acts only after Body grants substrate access, Eyes supplies enough
evidence, Soul keeps the invariant visible, Self routes authority, Life keeps
continuity recoverable, Face preserves public dignity when relevant, and
Imagination models the future shape.
- Mind admits state only after it can see which organ produced the thought and
which other organs shaped the proposal.
- Eyes supplies source-grounded evidence packets after Body grants access, so
the other organs do not mistake raw retrieval for inspected truth.
## Projector Ownership
The Projector is Imagination acting for Face. It turns typed state, Body facts,
social pressure, memory, and organ dependencies into a lived scene. It does not
speak publicly, grant repo access, or admit state.
The Face loop is:
```text
Body-gated facts + Mind state + social stimulus + organ dependencies
-> Imagination Projector
-> Face natural turn
-> Mind Interpreter
-> reviewed state effects / side effects / silence
```
Mind is the Interpreter and persistent state guardian. Body is the repository
access guardian. Eyes is the evidence ingress guardian. Imagination is the
Projector. Face is the public person. The dependency web is how the machine
avoids lonely little monarchies in costumes.

View file

@ -0,0 +1,329 @@
# Perfect Machine Audit Roadmap
Date: 2026-05-27
Objective: audit Epiphany's current organ shape and map the path from named
contracts to the Perfect Machine: a coherent organism where repository access,
evidence, projection, public speech, action, verification, continuity, and
persistent state are separately owned but mutually aware.
## Current Shape
Epiphany has the first typed skeleton for the organ boundaries:
- `Mind`: persistent state guardian. `epiphany-core::mind_gateway` names
thought, state-effect proposal, gateway-review, commit/rejection, and Verse
adoption contracts.
- `Body`: repository/substrate access guardian. `epiphany-core::body_gateway`
names repo access requests, reviews, grant/refusal receipts, snapshots, and
mutation receipts.
- `Eyes`: evidence ingress guardian. `epiphany-core::eyes_gateway` names
evidence requests, reviews, source lookup receipts, evidence packets, and
refusals.
- `Imagination`: projection/future-shape organ. For Face, it now owns the
Projector prompt boundary in `epiphany-core::face_turn`.
- `Face`: public person-shaped surface. It receives projected context and writes
natural prose only.
- `Hands`: action organ by doctrine, but not yet backed by a dedicated CultNet
contract family parallel to Body/Eyes/Mind.
- `Soul`: verification organ by doctrine, but not yet backed by a dedicated
CultNet contract family parallel to Eyes.
- `Life`: continuity organ by doctrine, heartbeat, reorientation, and handoff
surfaces, but not yet a single typed contract family for sleep/compaction and
recovery.
- `Self`: coordinator/routing organ exists through coordinator policy, role
board, CRRC, and view surfaces, but it is still partly embedded in bridge and
Codex-compatible routes.
The dependency matrix is explicit in `epiphany-core::organ_dependencies`: every
standing organ depends on every other standing organ. This is correct, but it
is currently descriptive. It does not yet force every launch packet, prompt, or
runtime receipt to carry dependency context.
## Void Face Lessons
Void's current Face work is the best reference implementation for the Face
organ. The important lesson is not "make the prompt longer." The lesson is
parent-organ ownership:
- Parent `Imagination` projects typed state into lived character-facing prose
before the Face sees it.
- The Face reads raw room/context evidence directly and writes natural prose as
a person.
- Parent `Mind` interprets the Face output into memory, affect, social reads,
mood, agency pressure, speech, retry, or drop.
- The deterministic assembler creates compact packets, raw transcripts, repo
activity, pronoun guidance, channel policy, and media awareness. It must not
turn those into character prose itself.
Relevant Void references:
- `VoidBot:prompts/repo-face-state-projector.prompt.md`: names the projector as
the Face's parent Imagination organ and forbids schema slurry.
- `VoidBot:prompts/repo-face-turn.prompt.md`: keeps the Face turn as the
speaking surface, with recent repo activity before conversation transcript and
deterministic policy sections outside projected state.
- `VoidBot:prompts/repo-face-turn-interpreter.prompt.md`: names the
Interpreter as parent Mind and requires route/retry/drop plus structured
side effects.
- `VoidBot:scripts/run-repo-face-heartbeats.ts`: runs projection as
`organ: "imagination"` in read-only mode, rejects leaky memory surfaces, keeps
raw transcript evidence oldest-to-newest, includes visible chronology, exports
recent repo activity read-only, and keeps pronoun guidance deterministic so
Imagination cannot accidentally omit it.
Audit implication for Epiphany: `face_turn.rs` has the right first boundary but
is still too thin. It lacks deterministic human pronoun guidance, channel label
resolution, visible chronology, media attachment awareness, retry/drop
semantics, projection model receipts, and explicit leakage rejection as strong
as Void's `rejectLeakyMemorySurface`.
## Audit Findings
### 1. Contract Names Exist Before Contract Execution
Mind, Body, and Eyes have type constants, CultMesh policy documents, and
runtime-spine advertisement. They are not yet the mandatory runtime path.
Workers can still be launched and accepted through older bridge/service flows
without every substrate touch, evidence claim, or state mutation passing through
a real receipt chain.
Required correction: turn contract families into executable gates.
### 2. Body/Eyes/Mind Are Correctly Split But Not Yet Chained
The desired chain is:
```text
Need fact or action
-> Self routes
-> Body grants scoped substrate access
-> Eyes packages inspected evidence when truth is needed
-> Imagination projects options or scenes when future/personhood is needed
-> Hands executes bounded action when mutation is needed
-> Soul verifies result/invariant
-> Mind admits durable state
-> Life preserves continuity
```
Today the chain is documented, not structurally unavoidable.
### 3. Hands, Soul, and Life Need First-Class Contracts
The current contract set names the gates around action but not action itself.
That leaves three weak organs:
- Hands needs action intent, execution receipt, patch receipt, command receipt,
commit/PR receipt, and refusal/rollback receipt contracts.
- Soul needs verification request, invariant check, evidence verdict,
regression/refusal, and review receipt contracts.
- Life needs compaction request, continuity packet, sleep/distillation,
recovery, stale-turn repair, and handoff receipt contracts.
Without those, Body/Mind/Eyes become paper gates around action and continuity
paths they do not own.
### 4. Self Still Shares A Throne With Compatibility Plumbing
Coordinator policy is much cleaner, but Codex-compatible route edges still
shape parts of the living workflow. The Perfect Machine needs Self as a typed
router over CultNet/CultMesh contracts, not a half-native coordinator behind a
JSON-RPC organ.
### 5. CultMesh/CultLib Local Dependency Is Broken
`epiphany-core` currently points at:
```toml
cultcache-rs = { path = "../../CultLib/crates/cultcache-rs" }
cultmesh-rs = { path = "../../CultLib/crates/cultmesh-rs" }
cultnet-rs = { path = "../../CultLib/crates/cultnet-rs" }
```
This checkout has `E:\Projects\CultLib` but no `crates` directory, so cargo
checks fail before reaching the new code. This is a Body-level substrate wound:
the Rust dependency body described by the map does not exist at the path the
machine uses.
Required correction: decide whether CultLib should restore `crates/` or
Epiphany should depend on the sibling standalone Rust repos. Do not make that
choice as a drive-by patch.
### 6. Face Prompting Is Directionally Correct But Behind Void
Epiphany's Face loop now says `Imagination Projector -> Face -> Mind
Interpreter`, but Void has the stronger living practice:
- state projector is model-owned, not deterministic prose assembly
- projection runs read-only with model/output receipts
- leaked schema/prompt construction language is rejected
- raw transcript and chronology stay raw and oldest-to-newest
- deterministic pronoun guidance sits outside projection
- channel labels are resolved through Face permissions before posting
- interpreter can retry one Face pass when correction uptake or doctrine fails
Epiphany should port that shape, not the exact TypeScript machinery.
## Path To The Perfect Machine
### Phase 0: Repair The Rust Body
Goal: make the existing contracts buildable again.
- Audit the CultLib/CultCache/CultNet/CultMesh repo layout.
- Choose the canonical Rust dependency source.
- Update dependency paths or restore the expected `CultLib/crates` layout.
- Run the focused tests that are currently blocked:
- `cargo test --manifest-path .\epiphany-core\Cargo.toml --lib face_turn`
- `cargo test --manifest-path .\epiphany-core\Cargo.toml --lib mind_gateway`
- `cargo test --manifest-path .\epiphany-core\Cargo.toml --lib body_gateway`
- `cargo test --manifest-path .\epiphany-core\Cargo.toml --lib eyes_gateway`
- `cargo test --manifest-path .\epiphany-core\Cargo.toml --lib cultmesh_integration`
- `cargo test --manifest-path .\epiphany-core\Cargo.toml --lib runtime_spine::tests::runtime_spine_emits_cultnet_hello_frame`
Definition of done: contract code compiles against the intended Rust substrate
without local path superstition.
### Phase 1: Make The Gates Executable
Goal: no worker output, repo touch, or evidence claim bypasses the appropriate
organ.
- Implement Body access request/review/grant/refusal documents in CultCache.
- Route retrieval, indexing, file edit, shell command, Rider, and Unity bridge
operations through Body access receipts.
- Implement Eyes evidence request/review/packet/refusal documents in CultCache.
- Require Eyes packets for claims promoted into Mind state proposals when the
claim depends on inspected source.
- Convert existing role/reorient acceptance to create Mind state-effect
proposals before state mutation, not merely a review attached to the old path.
Definition of done: the old direct paths cannot produce repo mutation,
evidence promotion, or durable state mutation without the corresponding typed
receipt.
### Phase 2: Give Hands, Soul, And Life Their Own Contracts
Goal: action, verification, and continuity stop hiding behind neighbors.
- Hands contracts:
- `epiphany.hands.action_intent`
- `epiphany.hands.command_receipt`
- `epiphany.hands.patch_receipt`
- `epiphany.hands.commit_receipt`
- `epiphany.hands.rollback_receipt`
- Soul contracts:
- `epiphany.soul.verification_request`
- `epiphany.soul.invariant_check`
- `epiphany.soul.verdict_receipt`
- `epiphany.soul.regression_receipt`
- Life contracts:
- `epiphany.life.continuity_packet`
- `epiphany.life.compaction_checkpoint`
- `epiphany.life.sleep_distillation`
- `epiphany.life.recovery_receipt`
- `epiphany.life.stale_turn_repair`
Definition of done: action, verification, and continuity have the same typed
contract dignity as Mind/Body/Eyes.
### Phase 3: Port Void's Face Prompting Shape Properly
Goal: Face becomes a living public organ without stealing authority.
- Replace `FaceProjectorInput` with an Imagination projector packet that carries
typed state, affect, memory, social topology, repo activity, semantic
attractors, curiosity hints, and dependency pressure.
- Add deterministic sections outside projected state:
- raw transcript, oldest-to-newest
- visible chronology across rooms/surfaces
- human pronoun guidance
- channel label/permission policy
- media attachment awareness
- recent home-repo activity from Body-gated reads
- Make projection model-owned and read-only, with model output receipts.
- Strengthen projection leakage rejection: no schema words, repo paths, prompt
construction language, grants/jurisdictions slurry, or direct action syntax.
- Upgrade Mind Interpreter output from simple effect blocks to:
- correction check
- doctrine/coherence check
- decision: route/retry/drop
- structured memory/affect/social/speech effects
- retry limit
- Ensure public speech resolves Face-local channel labels through permissions
before delivery.
Definition of done: Face can be prompted by Aquarium/Discord/CultNet and the
public result is natural, contextual, permission-aware, retryable, and unable to
mutate state or post without parent routing.
### Phase 4: Move Self Fully Onto CultNet/CultMesh
Goal: Self routes organ work through typed contracts, not legacy JSON-RPC
comfort tunnels.
- Add Self routing contracts for choosing next organ, required dependencies,
and allowed authority.
- Make role launch packets declare required Body/Eyes/Hands/Soul/Life/Mind
receipts.
- Make Aquarium read the contract catalog and available receipts instead of
hard-coding the route zoo.
- Starve `codex_message_processor` down to Codex auth/model transport and
compatibility emission only.
Definition of done: a local run can be inspected as a chain of CultNet/CultMesh
documents from operator request to final response.
### Phase 5: Runtime Physiology
Goal: the machine runs as an organism.
- Heartbeat scheduler wakes organs by initiative, pending pressure, and
completion-gated cooldown.
- Active turns freeze initiative until receipt completion.
- Sleep distills rumination through Life and Mind.
- Stale active turns get Life recovery receipts.
- Public/global Verse material enters as thought weather, then Eyes/Mind review
before adoption.
Definition of done: unattended operation can pause, recover, explain current
pressure, and preserve memory without transcript worship or hidden loops.
### Phase 6: Aquarium As Inspectable Nervous System
Goal: make the organism visible without making the UI a second truth.
- Display organ dependency graph.
- Display contract catalog by organ.
- Display receipt chains per turn.
- Display Face prompt packet boundaries: deterministic evidence, Imagination
projection, Face natural turn, Mind interpretation.
- Display Body grants, Eyes packets, Hands actions, Soul verdicts, Life
continuity, and Mind state commits.
Definition of done: a human can ask "why did you touch that file / say that /
remember that?" and see the typed path.
## Perfect Machine Target
The Perfect Machine is not more prompts and not more bureaucracy. It is a
coherent authority graph:
```text
Self routes.
Body grants substrate access.
Eyes certifies looked-at evidence.
Imagination projects possible scenes and futures.
Face speaks as a person.
Hands changes the world.
Soul verifies promises and invariants.
Life preserves continuity across rupture.
Mind admits durable state.
CultMesh carries the local typed Verse surfaces.
CultNet carries wire contracts.
CultCache preserves the typed documents.
Codex remains only model/auth transport until it can be replaced or minimized.
```
When this is real, no organ can steal the throne. The machine can act, speak,
remember, doubt, recover, and be inspected without turning connection into mush.

View file

@ -770,7 +770,7 @@ work so repo orientation stops beginning with blind `rg`.
Files:
- `epiphany-core/src/prompt.rs`
- `epiphany-state-model/src/prompt.rs`
- `epiphany-core/src/surfaces/worker_launch.rs`
- `vendor/codex/...` only as compatibility bridge surfaces if still needed

View file

@ -13,6 +13,35 @@ cosplaying as a Discord moderator. The useful organs are:
- semantic memory vectors in Qdrant with source hashes and compact dimensions
- memory resonance, incubation queues, analytic/associative thought lanes, and a
bridge that decides speech, draft, hold, or silence
- repo identities with Face state, Discord roles, pending mentions, jurisdiction,
proposal authority, and a CTB/Final-Fantasy-style initiative scheduler
## Project-Face Direction
VoidBot is no longer only a moderator with retrieval. It now has the small
working version of the Epiphany pitch: repos grow Faces.
The live pattern is:
- a repo identity has a Discord role and registered allowed channels
- that identity has repo-local Face state, not just base Void personality
- first contact can birth the Face through Epiphany repo terrain/personality
scouting and reviewable startup artifacts
- role/display-name mentions become pending obligations, not direct hidden jobs
- an initiative scheduler chooses which Face or system agent gets the next turn
- heat can prioritize identities, repos, participant kinds, turns, channels, or
all participants
- active turns freeze initiative until the owner-Codex job or moderation lock
completes
- Faces may advocate for repo needs and propose changes, while implementation
authority still requires the configured human/review path
Epiphany should absorb that as native product direction. The user should talk to
projects: "Aqua, what does AquaSynth need?", "Nibu, what is wrong with this lore
arc?", "Mimir, what seam is drifting?" The project should schedule modeling,
research, verification, memory maintenance, or a Face response from its own
typed state. The human should not have to deliver a complete implementation
brief just to make the system start thinking.
## Target Shape
@ -58,6 +87,12 @@ The completion receipt is the only normal path that starts cooldown. This is the
part that keeps the heart from turning into a tiny manager with a stopwatch and
no shame.
Repo Faces use the same law. A Face can be heated because a human mentioned it,
a repo is hot, a stream is live, or a group needs attention; that heat changes
opportunity, not authority. The Face still freezes while thinking, still returns
through typed receipts, and still needs an explicit grant before crossing from
proposal into canonical repo mutation.
## Persistence Contract
Epiphany needs a Rust version of the CultCache contract because the app-server

View file

@ -21,6 +21,10 @@ developer's damp recollection.
- [dossier-profiles.md](./dossier-profiles.md):
explicit split between lean Epiphany work-organ dossiers and embodied
Ghostlight/Face personalities.
- [agent-utterance-state-schema.md](./agent-utterance-state-schema.md):
derived speech-conditioning subset for Weks, Aquarium, and other utterance
surfaces; it carries identity, trait vectors, mood, and activation without
memory records.
- [repo-personality-birth-projection.md](./repo-personality-birth-projection.md):
deterministic birth-time path from repo terrain to newborn trait lattice and
heartbeat seeds.

View file

@ -0,0 +1,144 @@
# Agent Utterance State Schema
`epiphany.agent_utterance_state.v0` is the compact speech-conditioning view for
Weks, Aquarium, Discord/voice bridges, and any other surface that needs an agent
to sound like itself without ingesting the whole Ghostlight dossier.
It is a derived document. The durable source of truth stays split where it
belongs:
- canonical identity, values, and trait vectors live in the Ghostlight-shaped
role dossier (`EpiphanyAgentMemoryEntry`)
- current load, heartbeat heat, cooldown multipliers, pending-turn state, and
mood pressure live in heartbeat participant state
- utterance systems consume the derived subset and do not read memories,
events, scenes, or relationship-memory records
## Shape
The document carries:
- `identity`: public name, roles, origin, and public description only
- `personalityVectors`: the six canonical trait-vector families used for voice
and behavior projection
- `values`: value labels and priorities that should bend speech tone
- `currentMood`: mood label plus a 32-axis named emotional state projection
covering valence, arousal, dominance, urgency, anger, despair, sadness, fear,
anxiety, disgust, contempt, annoyance, dismissal, flippancy, playfulness,
irony, tenderness, warmth, joy, excitement, fatigue, guardedness, confidence,
shame, pride, threat, secrecy, hesitation, emotional containment, thought
pressure, reaction intensity, and command force
- `activation`: awake/running status, current load, initiative speed,
interruptibility, cooldown multipliers, heat multiplier, pending-turn flag,
and last wake/finish timestamps
- `characterStateVector`: the 64-float deterministic
`weksa.utterance_embedding_handoff.v0.1` speaker-state lane; first 32 slots
are the current emotional state projection, later slots add activation and
slower speaker traits/context, with slot labels and uncertainties kept beside
the values
- `utteranceEmbeddingBasis`: bounded weighted text snippets generated from the
same typed fields for utterance embedding input
## Invariants
- No episodic memories.
- No semantic memories.
- No relationship summaries or linked memory ids.
- No private notes.
- No hidden JSON cargo.
- The `characterStateVector.values` array is exactly 64 floats. Resizing or
reordering slots is a schema-version change.
- The document may be serialized for CultNet, but in-process callers should use
the typed Rust structs.
## Weksa/AquaSynth Input Vectors
The full Weksa handoff is `weksa.utterance_embedding_handoff.v0.1`. The input
vector widths are fixed:
- `speech_text_embedding`: 1024 floats from `bge-m3:latest`; text semantics,
not phonetics.
- `phonetic_realization_vector`: 256 floats from Weksa/PanPhon-style phonetic
realization; IPA/phone/speech-shape evidence, not text meaning.
- `prosody_emphasis_hints`: 32 floats; Weksa-owned delivery hints.
- `character_state_vector`: 64 floats; Epiphany/Ghostlight-shaped speaker
state from this document.
- `utterance_embedding`: 64 floats; AquaSynth-owned learned output, not an
Epiphany input.
## Character State Vector Slots
`characterStateVector.values` uses these exact `0..63` slots for
`weksa.utterance_embedding_handoff.v0.1` compatibility:
| Index | Name | Source |
| --- | --- | --- |
| 0 | `valence` | `currentMood.emotionalDimensions.valence` |
| 1 | `arousal` | `currentMood.emotionalDimensions.arousal` |
| 2 | `dominance` | `currentMood.emotionalDimensions.dominance` |
| 3 | `urgency` | `currentMood.emotionalDimensions.urgency` |
| 4 | `anger` | `currentMood.emotionalDimensions.anger` |
| 5 | `despair` | `currentMood.emotionalDimensions.despair` |
| 6 | `sadness` | `currentMood.emotionalDimensions.sadness` |
| 7 | `fear` | `currentMood.emotionalDimensions.fear` |
| 8 | `anxiety` | `currentMood.emotionalDimensions.anxiety` |
| 9 | `disgust` | `currentMood.emotionalDimensions.disgust` |
| 10 | `contempt` | `currentMood.emotionalDimensions.contempt` |
| 11 | `annoyance` | `currentMood.emotionalDimensions.annoyance` |
| 12 | `dismissal` | `currentMood.emotionalDimensions.dismissal` |
| 13 | `flippancy` | `currentMood.emotionalDimensions.flippancy` |
| 14 | `playfulness` | `currentMood.emotionalDimensions.playfulness` |
| 15 | `irony` | `currentMood.emotionalDimensions.irony` |
| 16 | `tenderness` | `currentMood.emotionalDimensions.tenderness` |
| 17 | `warmth` | `currentMood.emotionalDimensions.warmth` |
| 18 | `joy` | `currentMood.emotionalDimensions.joy` |
| 19 | `excitement` | `currentMood.emotionalDimensions.excitement` |
| 20 | `fatigue` | `currentMood.emotionalDimensions.fatigue` |
| 21 | `guardedness` | `currentMood.emotionalDimensions.guardedness` |
| 22 | `confidence` | `currentMood.emotionalDimensions.confidence` |
| 23 | `shame` | `currentMood.emotionalDimensions.shame` |
| 24 | `pride` | `currentMood.emotionalDimensions.pride` |
| 25 | `threat` | `currentMood.emotionalDimensions.threat` |
| 26 | `secrecy` | `currentMood.emotionalDimensions.secrecy` |
| 27 | `hesitation` | `currentMood.emotionalDimensions.hesitation` |
| 28 | `emotionalContainment` | `currentMood.emotionalDimensions.emotionalContainment` |
| 29 | `thoughtPressure` | `currentMood.emotionalDimensions.thoughtPressure` |
| 30 | `reactionIntensity` | `currentMood.emotionalDimensions.reactionIntensity` |
| 31 | `commandForce` | `currentMood.emotionalDimensions.commandForce` |
| 32 | `currentLoad` | `activation.currentLoad` |
| 33 | `initiativeHeat` | `activation.initiativeHeatMultiplier` |
| 34 | `pendingTurn` | `activation.pendingTurnActive` |
| 35 | `cooldownPressure` | `activation.effectiveCooldownMultiplier` |
| 36 | `reactionBias` | `activation.reactionBias` |
| 37 | `interruptThreshold` | `activation.interruptThreshold` |
| 38 | `initiativeSpeed` | `activation.initiativeSpeed` |
| 39 | `voiceBubbly` | `personalityVectors.voiceStyle.bubbly_pushiness` |
| 40 | `voiceDryness` | `personalityVectors.voiceStyle.dryness` |
| 41 | `voiceFormality` | `personalityVectors.voiceStyle.formality` |
| 42 | `voiceDirectness` | `personalityVectors.voiceStyle.directness` |
| 43 | `voiceIntensity` | `personalityVectors.voiceStyle.intensity` |
| 44 | `voiceWarmth` | `personalityVectors.voiceStyle.warmth` |
| 45 | `voicePrecision` | `personalityVectors.voiceStyle.precision` |
| 46 | `voiceRitualRegister` | `personalityVectors.voiceStyle.ritual_register` |
| 47 | `presentationPushiness` | `personalityVectors.presentationStrategy.pushiness` |
| 48 | `presentationSelfFocus` | `personalityVectors.presentationStrategy.self_focus` |
| 49 | `presentationPlay` | `personalityVectors.presentationStrategy.play` |
| 50 | `presentationCareDemand` | `personalityVectors.presentationStrategy.care_demand` |
| 51 | `behaviorWorkDrive` | `personalityVectors.behavioralDimensions.work_drive` |
| 52 | `behaviorBoundaryDefense` | `personalityVectors.behavioralDimensions.boundary_defense` |
| 53 | `behaviorPurityDrive` | `personalityVectors.behavioralDimensions.purity_drive` |
| 54 | `behaviorPatience` | `personalityVectors.behavioralDimensions.patience` |
| 55 | `stableAgreeableness` | `personalityVectors.stableDispositions.agreeableness` |
| 56 | `stableConscientiousness` | `personalityVectors.stableDispositions.conscientiousness` |
| 57 | `stableNeuroticism` | `personalityVectors.stableDispositions.neuroticism` |
| 58 | `stableOpenness` | `personalityVectors.stableDispositions.openness` |
| 59 | `organizationCoherence` | `personalityVectors.underlyingOrganization.coherence` |
| 60 | `organizationRigidity` | `personalityVectors.underlyingOrganization.rigidity` |
| 61 | `slowTraitReserved62` | `reserved.slowTrait.62` |
| 62 | `slowTraitReserved63` | `reserved.slowTrait.63` |
| 63 | `slowTraitReserved64` | `reserved.slowTrait.64` |
If a caller needs autobiographical recall, it should ask the memory graph for a
separate context cut. Do not fatten this document until it becomes a second
dossier wearing stage makeup. That is how the old machine learned to lie
politely.

View file

@ -5,6 +5,13 @@ other swarm-side observers. It contains the runtime state, operator-facing
surface projections, control intents, and receipt/artifact payloads that the
runtime-spine advertises over `cultnet.schema.v0`.
The product direction is project-native agency: clients should speak to a
project or one of its Faces, then watch typed scheduling, memory, evidence, and
authority surfaces do the structuring work. Aquarium is the most direct client,
but Discord, voice/WebRTC rooms, stream overlays, native CLIs, and other trusted
tools should discover the same contracts instead of inventing private command
verbs.
## What Lives Here
- `index.json`: local registration manifest loaded by
@ -21,12 +28,14 @@ server object deserves to be duplicated into a second baroque schema maze.
- `epiphany.runtime.*`: native runtime-spine identity, session, job, job
result, and event documents.
- `epiphany.agent_*` and `epiphany.state-ledger`: durable typed state the swarm
actually lives on.
actually lives on, plus derived agent views such as
`epiphany.agent_utterance_state.v0` for speech-conditioning without memory
payloads.
- `epiphany.surface.*`: operator-safe projections for scene, freshness,
context, graph query, coordinator, roles, jobs, Face, Rider, Unity, repo
initialization, and related live surfaces.
- `*.intent.v0`: control documents Aquarium or another trusted coordinator can
submit through CultNet.
- `*.intent.v0`: control documents Aquarium, a Discord bridge, a voice surface,
or another trusted coordinator can submit through CultNet.
- receipt/artifact schemas such as `epiphany.swarm-control-receipt`,
`epiphany.face-bubble`, `epiphany.character-turn-packet`, and
`epiphany.repo-birth-runner`.

View file

@ -23,6 +23,9 @@
"type": "object",
"additionalProperties": true
},
"initiativeHeat": {
"$ref": "epiphany.heartbeat_initiative_heat.v0.schema.json"
},
"participants": {
"type": "array",
"items": {

View file

@ -0,0 +1,451 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.agent-utterance-state.schema.json",
"title": "Epiphany Agent Utterance State",
"description": "Derived speech-conditioning subset of a Ghostlight agent dossier plus heartbeat mood/activation. Excludes memories.",
"type": "object",
"additionalProperties": false,
"properties": {
"schemaVersion": {
"const": "epiphany.agent_utterance_state.v0"
},
"source": {
"type": "string"
},
"roleId": {
"type": "string"
},
"agentId": {
"type": "string"
},
"dossierProfile": {
"type": "object",
"additionalProperties": false,
"properties": {
"profileKind": {
"enum": [
"lane_core",
"embodied_actor"
]
},
"canonicalDensity": {
"type": "string"
},
"relationshipModel": {
"type": "string"
},
"perceivedOverlayMode": {
"type": "string"
},
"growthChannels": {
"type": "array",
"items": {
"type": "string"
}
},
"notes": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"profileKind",
"canonicalDensity",
"relationshipModel",
"perceivedOverlayMode",
"growthChannels",
"notes"
]
},
"identity": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"roles": {
"type": "array",
"items": {
"type": "string"
}
},
"origin": {
"type": "string"
},
"publicDescription": {
"type": "string"
}
},
"required": [
"name",
"roles",
"origin",
"publicDescription"
]
},
"personalityVectors": {
"$ref": "#/definitions/personalityVectors"
},
"values": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"valueId": {
"type": "string"
},
"label": {
"type": "string"
},
"priority": {
"type": "number"
},
"unforgivableIfBetrayed": {
"type": "boolean"
}
},
"required": [
"valueId",
"label",
"priority",
"unforgivableIfBetrayed"
]
}
},
"currentMood": {
"type": "object",
"additionalProperties": false,
"properties": {
"label": {
"type": "string"
},
"source": {
"type": "string"
},
"emotionalDimensions": {
"type": "array",
"minItems": 32,
"maxItems": 32,
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "number"
},
"sourcePath": {
"type": "string"
}
},
"required": [
"name",
"value",
"sourcePath"
]
}
},
"anxiety": {
"type": "number"
},
"urgency": {
"type": "number"
},
"arousal": {
"type": "number"
},
"thoughtPressure": {
"type": "number"
},
"guardedness": {
"type": "number"
},
"reactionIntensity": {
"type": "number"
},
"cooldownMultiplier": {
"type": "number"
}
},
"required": [
"label",
"emotionalDimensions",
"anxiety",
"urgency",
"arousal",
"thoughtPressure",
"guardedness",
"reactionIntensity",
"cooldownMultiplier"
]
},
"activation": {
"type": "object",
"additionalProperties": false,
"properties": {
"status": {
"type": "string"
},
"currentLoad": {
"type": "number"
},
"initiativeSpeed": {
"type": "number"
},
"reactionBias": {
"type": "number"
},
"interruptThreshold": {
"type": "number"
},
"personalityCooldownMultiplier": {
"type": "number"
},
"moodCooldownMultiplier": {
"type": "number"
},
"initiativeHeatMultiplier": {
"type": "number"
},
"effectiveCooldownMultiplier": {
"type": "number"
},
"pendingTurnActive": {
"type": "boolean"
},
"sceneId": {
"type": "string"
},
"lastWokeAt": {
"type": "string"
},
"lastFinishedAt": {
"type": "string"
}
},
"required": [
"status",
"currentLoad",
"initiativeSpeed",
"reactionBias",
"interruptThreshold",
"personalityCooldownMultiplier",
"moodCooldownMultiplier",
"initiativeHeatMultiplier",
"effectiveCooldownMultiplier",
"pendingTurnActive"
]
},
"characterStateVector": {
"type": "object",
"additionalProperties": false,
"properties": {
"schemaVersion": {
"const": "epiphany.agent_character_state_vector.v0"
},
"compatibleHandoffSchema": {
"const": "weksa.utterance_embedding_handoff.v0.1"
},
"source": {
"type": "string"
},
"dimensionality": {
"const": 64
},
"values": {
"type": "array",
"minItems": 64,
"maxItems": 64,
"items": {
"type": "number"
}
},
"slots": {
"type": "array",
"minItems": 64,
"maxItems": 64,
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"index": {
"type": "integer",
"minimum": 0,
"maximum": 63
},
"name": {
"type": "string"
},
"value": {
"type": "number"
},
"sourcePath": {
"type": "string"
}
},
"required": [
"index",
"name",
"value",
"sourcePath"
]
}
},
"auditProjection": {
"type": "object",
"additionalProperties": false,
"properties": {
"currentStatePressure": {
"type": "array",
"items": {
"type": "string"
}
},
"speakerTraitPressure": {
"type": "array",
"items": {
"type": "string"
}
},
"deliveryShape": {
"type": "string"
}
},
"required": [
"currentStatePressure",
"speakerTraitPressure",
"deliveryShape"
]
},
"uncertainties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"schemaVersion",
"compatibleHandoffSchema",
"source",
"dimensionality",
"values",
"slots",
"auditProjection",
"uncertainties"
]
},
"utteranceEmbeddingBasis": {
"type": "array",
"maxItems": 32,
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"kind": {
"type": "string"
},
"path": {
"type": "string"
},
"weight": {
"type": "number"
},
"text": {
"type": "string"
}
},
"required": [
"kind",
"path",
"weight",
"text"
]
}
},
"contract": {
"type": "string"
}
},
"required": [
"schemaVersion",
"source",
"roleId",
"agentId",
"dossierProfile",
"identity",
"personalityVectors",
"values",
"currentMood",
"activation",
"characterStateVector",
"utteranceEmbeddingBasis",
"contract"
],
"definitions": {
"personalityVectors": {
"type": "object",
"additionalProperties": false,
"properties": {
"underlyingOrganization": {
"$ref": "#/definitions/traitVectorMap"
},
"stableDispositions": {
"$ref": "#/definitions/traitVectorMap"
},
"behavioralDimensions": {
"$ref": "#/definitions/traitVectorMap"
},
"presentationStrategy": {
"$ref": "#/definitions/traitVectorMap"
},
"voiceStyle": {
"$ref": "#/definitions/traitVectorMap"
},
"situationalState": {
"$ref": "#/definitions/traitVectorMap"
}
},
"required": [
"underlyingOrganization",
"stableDispositions",
"behavioralDimensions",
"presentationStrategy",
"voiceStyle",
"situationalState"
]
},
"traitVectorMap": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/traitVector"
}
},
"traitVector": {
"type": "object",
"additionalProperties": false,
"properties": {
"mean": {
"type": "number"
},
"plasticity": {
"type": "number"
},
"current_activation": {
"type": "number"
}
},
"required": [
"mean",
"plasticity",
"current_activation"
]
}
}
}

View file

@ -23,6 +23,9 @@
"type": "object",
"additionalProperties": true
},
"utteranceState": {
"$ref": "epiphany.agent-utterance-state.schema.json"
},
"projectedLocalContext": {
"type": "object",
"additionalProperties": true
@ -66,6 +69,7 @@
"source",
"createdAt",
"stimulus",
"utteranceState",
"projectedLocalContext",
"projectionSeed",
"appraisalSeed",

View file

@ -0,0 +1,82 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.heartbeat_heat_intent.v0.schema.json",
"title": "epiphany heartbeat_heat_intent v0",
"description": "Coordinator intent for changing heartbeat initiative heat at global, group, arena, role, or agent scope.",
"type": "object",
"additionalProperties": true,
"properties": {
"threadId": {
"type": "string"
},
"storeKey": {
"type": [
"string",
"null"
],
"description": "Optional heartbeat store key when the coordinator hosts more than one heartbeat arena."
},
"scope": {
"$ref": "epiphany.heartbeat_initiative_heat.v0.schema.json#/definitions/scope"
},
"selector": {
"type": "string",
"description": "Agent id, role id, arena, participant kind, group, or constraint selected by scope. Empty only for global/all."
},
"multiplier": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Recovery-speed multiplier. Values above 1 turn up heat; values below 1 cool the selected lane."
},
"id": {
"type": [
"string",
"null"
],
"description": "Stable policy id. Defaults to global or scope:selector when omitted."
},
"label": {
"type": [
"string",
"null"
]
},
"reason": {
"type": [
"string",
"null"
]
},
"expiresAfterSceneClock": {
"type": [
"number",
"null"
],
"description": "Relative scene-clock duration before the multiplier stops affecting participants."
},
"clear": {
"type": "boolean",
"default": false
}
},
"required": [
"threadId",
"scope"
],
"allOf": [
{
"if": {
"properties": {
"clear": {
"const": false
}
}
},
"then": {
"required": [
"multiplier"
]
}
}
]
}

View file

@ -0,0 +1,91 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.heartbeat_initiative_heat.v0.schema.json",
"title": "epiphany heartbeat_initiative_heat v0",
"description": "Heartbeat initiative heat policy. Multipliers accelerate or relax turn recovery without granting project authority.",
"type": "object",
"additionalProperties": true,
"properties": {
"schemaVersion": {
"const": "epiphany.heartbeat_initiative_heat.v0"
},
"globalMultiplier": {
"type": "number",
"exclusiveMinimum": 0
},
"multipliers": {
"type": "array",
"items": {
"$ref": "#/definitions/multiplier"
}
}
},
"required": [
"schemaVersion",
"globalMultiplier",
"multipliers"
],
"definitions": {
"scope": {
"type": "string",
"enum": [
"global",
"all",
"agent",
"agent_id",
"role",
"role_id",
"arena",
"participant_kind",
"kind",
"group",
"constraint"
]
},
"multiplier": {
"type": "object",
"additionalProperties": true,
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"label": {
"type": "string"
},
"scope": {
"$ref": "#/definitions/scope"
},
"selector": {
"type": "string"
},
"multiplier": {
"type": "number",
"exclusiveMinimum": 0
},
"reason": {
"type": "string"
},
"updatedAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"expiresAtSceneClock": {
"type": [
"number",
"null"
]
}
},
"required": [
"id",
"scope",
"selector",
"multiplier"
]
}
}
}

View file

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.job_interrupt_intent.v0.schema.json",
"title": "epiphany job_interrupt_intent v0",
"description": "Bounded job interrupt intent mirroring thread/epiphany/jobInterrupt.",
"description": "Bounded typed job interrupt intent.",
"type": "object",
"additionalProperties": true,
"properties": {

View file

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.job_launch_intent.v0.schema.json",
"title": "epiphany job_launch_intent v0",
"description": "Bounded job launch intent mirroring thread/epiphany/jobLaunch.",
"description": "Bounded typed job launch intent.",
"type": "object",
"additionalProperties": true,
"properties": {

View file

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://gamecult.dev/epiphany/cultnet/epiphany.reorient_accept_intent.v0.schema.json",
"title": "epiphany reorient_accept_intent v0",
"description": "Review-gated reorientation finding acceptance intent mirroring thread/epiphany/reorientAccept.",
"description": "Review-gated typed reorientation finding acceptance intent.",
"type": "object",
"additionalProperties": true,
"properties": {

Some files were not shown because too many files have changed in this diff Show more