Add Epiphany investigation checkpoints

This commit is contained in:
Meta Vens 2026-04-26 05:12:51 +01:00
commit 71ea1ed2ea
17 changed files with 578 additions and 40 deletions

View file

@ -5,6 +5,7 @@ use codex_protocol::protocol::EpiphanyGraphCheckpoint;
use codex_protocol::protocol::EpiphanyGraphFrontier;
use codex_protocol::protocol::EpiphanyGraphs;
use codex_protocol::protocol::EpiphanyInvariant;
use codex_protocol::protocol::EpiphanyInvestigationCheckpoint;
use codex_protocol::protocol::EpiphanyObservation;
use codex_protocol::protocol::EpiphanySubgoal;
use std::collections::HashSet;
@ -18,6 +19,7 @@ pub struct EpiphanyPromotionInput {
pub graphs: Option<EpiphanyGraphs>,
pub graph_frontier: Option<EpiphanyGraphFrontier>,
pub graph_checkpoint: Option<EpiphanyGraphCheckpoint>,
pub investigation_checkpoint: Option<EpiphanyInvestigationCheckpoint>,
pub churn: Option<EpiphanyChurnState>,
pub observations: Vec<EpiphanyObservation>,
pub evidence: Vec<EpiphanyEvidenceRecord>,
@ -38,6 +40,8 @@ pub struct EpiphanyStateReplacementValidationInput<'a> {
pub graphs: Option<&'a EpiphanyGraphs>,
pub graph_frontier: Option<&'a EpiphanyGraphFrontier>,
pub graph_checkpoint: Option<&'a EpiphanyGraphCheckpoint>,
pub investigation_checkpoint: Option<&'a EpiphanyInvestigationCheckpoint>,
pub available_evidence_ids: Option<&'a HashSet<&'a str>>,
pub churn: Option<&'a EpiphanyChurnState>,
}
@ -101,6 +105,8 @@ pub fn evaluate_promotion(input: EpiphanyPromotionInput) -> EpiphanyPromotionDec
graphs: input.graphs.as_ref(),
graph_frontier: input.graph_frontier.as_ref(),
graph_checkpoint: input.graph_checkpoint.as_ref(),
investigation_checkpoint: input.investigation_checkpoint.as_ref(),
available_evidence_ids: Some(&evidence_ids),
churn: input.churn.as_ref(),
},
&mut reasons,
@ -182,6 +188,9 @@ fn validate_state_replacements(
validate_checkpoint_shape(checkpoint, reasons);
}
}
if let Some(checkpoint) = input.investigation_checkpoint {
validate_investigation_checkpoint(checkpoint, input.available_evidence_ids, reasons);
}
if let Some(churn) = input.churn {
validate_churn(churn, reasons);
}
@ -450,6 +459,67 @@ fn validate_churn(churn: &EpiphanyChurnState, reasons: &mut Vec<String>) {
require_nonempty(&churn.diff_pressure, "patch.churn.diff_pressure", reasons);
}
fn validate_investigation_checkpoint(
checkpoint: &EpiphanyInvestigationCheckpoint,
available_evidence_ids: Option<&HashSet<&str>>,
reasons: &mut Vec<String>,
) {
require_nonempty(
&checkpoint.checkpoint_id,
"patch.investigation_checkpoint.checkpoint_id",
reasons,
);
require_nonempty(
&checkpoint.kind,
"patch.investigation_checkpoint.kind",
reasons,
);
require_nonempty(
&checkpoint.focus,
"patch.investigation_checkpoint.focus",
reasons,
);
if let Some(summary) = checkpoint.summary.as_deref() {
require_nonempty(summary, "patch.investigation_checkpoint.summary", reasons);
}
if let Some(next_action) = checkpoint.next_action.as_deref() {
require_nonempty(
next_action,
"patch.investigation_checkpoint.next_action",
reasons,
);
} else {
reasons.push("patch.investigation_checkpoint.next_action must not be empty".to_string());
}
if let Some(turn_id) = checkpoint.captured_at_turn_id.as_deref() {
require_nonempty(
turn_id,
"patch.investigation_checkpoint.captured_at_turn_id",
reasons,
);
}
require_unique_nonempty(
"patch.investigation_checkpoint.open_questions",
checkpoint.open_questions.iter().map(String::as_str),
reasons,
);
require_unique_nonempty(
"patch.investigation_checkpoint.evidence_ids",
checkpoint.evidence_ids.iter().map(String::as_str),
reasons,
);
if let Some(available_evidence_ids) = available_evidence_ids {
for evidence_id in &checkpoint.evidence_ids {
if !available_evidence_ids.contains(evidence_id.as_str()) {
reasons.push(format!(
"investigation checkpoint cites missing evidence id {:?}",
evidence_id
));
}
}
}
}
fn graph_node_ids(graphs: &EpiphanyGraphs) -> HashSet<&str> {
graphs
.architecture
@ -584,6 +654,7 @@ mod tests {
graphs: None,
graph_frontier: None,
graph_checkpoint: None,
investigation_checkpoint: None,
churn: None,
observations,
evidence,
@ -627,6 +698,7 @@ mod tests {
graphs: None,
graph_frontier: None,
graph_checkpoint: None,
investigation_checkpoint: None,
churn: None,
observations: vec![EpiphanyObservation {
id: "obs-1".to_string(),
@ -728,6 +800,8 @@ mod tests {
graphs: Some(&graphs),
graph_frontier: Some(&frontier),
graph_checkpoint: None,
investigation_checkpoint: None,
available_evidence_ids: None,
churn: None,
});
@ -738,6 +812,37 @@ mod tests {
);
}
#[test]
fn validate_state_replacement_patch_rejects_investigation_checkpoint_with_missing_evidence() {
let checkpoint = EpiphanyInvestigationCheckpoint {
checkpoint_id: "ix-1".to_string(),
kind: "source_gathering".to_string(),
focus: "Trace the compaction seam.".to_string(),
next_action: Some("Re-gather source before editing.".to_string()),
evidence_ids: vec!["ev-missing".to_string()],
..Default::default()
};
let available_evidence_ids = HashSet::from(["ev-present"]);
let reasons = validate_state_replacement_patch(EpiphanyStateReplacementValidationInput {
active_subgoal_id: None,
subgoals: None,
invariants: None,
graphs: None,
graph_frontier: None,
graph_checkpoint: None,
investigation_checkpoint: Some(&checkpoint),
available_evidence_ids: Some(&available_evidence_ids),
churn: None,
});
assert!(
reasons
.iter()
.any(|reason| reason.contains("missing evidence id"))
);
}
#[test]
fn evaluate_promotion_accepts_verified_map_and_churn_replacement() {
let decision = evaluate_promotion(EpiphanyPromotionInput {
@ -769,6 +874,23 @@ mod tests {
assert!(decision.accepted, "{:?}", decision.reasons);
}
#[test]
fn evaluate_promotion_accepts_investigation_checkpoint_when_backed_by_evidence() {
let decision = evaluate_promotion(EpiphanyPromotionInput {
investigation_checkpoint: Some(EpiphanyInvestigationCheckpoint {
checkpoint_id: "ix-2".to_string(),
kind: "slice_planning".to_string(),
focus: "Map the next bounded edit before compaction.".to_string(),
next_action: Some("Resume from this packet if pressure stays stable.".to_string()),
evidence_ids: vec!["ev-1".to_string()],
..Default::default()
}),
..promotion_input(true, vec![observation()], vec![evidence()], "ok")
});
assert!(decision.accepted, "{:?}", decision.reasons);
}
#[test]
fn evaluate_promotion_rejects_risky_churn_without_warning() {
let decision = evaluate_promotion(EpiphanyPromotionInput {

View file

@ -7,6 +7,8 @@ use codex_protocol::protocol::EpiphanyGraphEdge;
use codex_protocol::protocol::EpiphanyGraphFrontier;
use codex_protocol::protocol::EpiphanyGraphLink;
use codex_protocol::protocol::EpiphanyGraphNode;
use codex_protocol::protocol::EpiphanyInvestigationCheckpoint;
use codex_protocol::protocol::EpiphanyInvestigationDisposition;
use codex_protocol::protocol::EpiphanyModeState;
use codex_protocol::protocol::EpiphanyObservation;
use codex_protocol::protocol::EpiphanyScratchPad;
@ -51,6 +53,11 @@ pub fn render_epiphany_state(state: &EpiphanyThreadState) -> String {
if let Some(graphs) = render_graphs(state) {
sections.push(graphs);
}
if let Some(checkpoint) =
render_investigation_checkpoint(state.investigation_checkpoint.as_ref())
{
sections.push(checkpoint);
}
if let Some(scratch) = render_scratch(state.scratch.as_ref()) {
sections.push(scratch);
}
@ -393,6 +400,56 @@ fn render_scratch(scratch: Option<&EpiphanyScratchPad>) -> Option<String> {
}
}
fn render_investigation_checkpoint(
checkpoint: Option<&EpiphanyInvestigationCheckpoint>,
) -> Option<String> {
let checkpoint = checkpoint?;
let mut lines = vec![
format!("- Id: `{}`", checkpoint.checkpoint_id),
format!("- Kind: {}", compact_text(&checkpoint.kind)),
format!(
"- Disposition: {}",
match checkpoint.disposition {
EpiphanyInvestigationDisposition::ResumeReady => "resume_ready",
EpiphanyInvestigationDisposition::RegatherRequired => "regather_required",
}
),
format!("- Focus: {}", compact_text(&checkpoint.focus)),
];
if let Some(summary) = checkpoint.summary.as_deref() {
lines.push(format!("- Summary: {}", compact_text(summary)));
}
if let Some(next_action) = checkpoint.next_action.as_deref() {
lines.push(format!("- Next action: {}", compact_text(next_action)));
}
if let Some(turn_id) = checkpoint.captured_at_turn_id.as_deref() {
lines.push(format!("- Captured at turn: `{turn_id}`"));
}
if !checkpoint.open_questions.is_empty() {
lines.push(format!(
"- Open questions: {}",
checkpoint
.open_questions
.iter()
.map(|question| compact_text(question))
.collect::<Vec<_>>()
.join(" | ")
));
}
if !checkpoint.evidence_ids.is_empty() {
lines.push(format!(
"- Evidence ids: {}",
render_inline_ids(&checkpoint.evidence_ids)
));
}
let code_refs = render_code_refs(&checkpoint.code_refs);
if !code_refs.is_empty() {
lines.push(format!("- Code refs: {}", code_refs.join(", ")));
}
Some(format!("## Investigation Checkpoint\n{}", lines.join("\n")))
}
fn render_observations(observations: &[EpiphanyObservation]) -> Option<String> {
if observations.is_empty() {
return None;
@ -675,6 +732,8 @@ mod tests {
use codex_protocol::protocol::EpiphanyGraphFrontier;
use codex_protocol::protocol::EpiphanyGraphNode;
use codex_protocol::protocol::EpiphanyGraphs;
use codex_protocol::protocol::EpiphanyInvestigationCheckpoint;
use codex_protocol::protocol::EpiphanyInvestigationDisposition;
use codex_protocol::protocol::EpiphanyModeState;
use codex_protocol::protocol::EpiphanyThreadState;
use std::path::PathBuf;
@ -754,6 +813,27 @@ mod tests {
frontier_node_ids: vec!["arch-session".to_string()],
..Default::default()
}),
investigation_checkpoint: Some(EpiphanyInvestigationCheckpoint {
checkpoint_id: "ix-1".to_string(),
kind: "slice_planning".to_string(),
disposition: EpiphanyInvestigationDisposition::ResumeReady,
focus: "Map the state update seam before broad edits.".to_string(),
next_action: Some(
"Re-read the write path, then patch one bounded field.".to_string(),
),
open_questions: vec![
"Do scene and context both need the resume packet?".to_string(),
],
code_refs: vec![EpiphanyCodeRef {
path: PathBuf::from("core/src/codex_thread.rs"),
start_line: Some(376),
end_line: Some(424),
symbol: Some("epiphany_update_state".to_string()),
note: Some("Checkpoint writes must use the same red pen.".to_string()),
}],
evidence_ids: vec!["ev-1".to_string()],
..Default::default()
}),
churn: Some(EpiphanyChurnState {
understanding_status: "stable".to_string(),
diff_pressure: "low".to_string(),
@ -774,6 +854,9 @@ mod tests {
assert!(rendered.contains("`flow-build-context`"));
assert!(rendered.contains("Do not build the Jenga tower"));
assert!(rendered.contains("revert it before trying the next hypothesis"));
assert!(rendered.contains("## Investigation Checkpoint"));
assert!(rendered.contains("resume_ready"));
assert!(rendered.contains("Map the state update seam before broad edits."));
assert!(!rendered.contains("## Scratch"));
assert!(!rendered.contains("## Invariants"));
assert!(rendered.contains("`core/src/session/mod.rs:2433-2617`"));

View file

@ -1661,6 +1661,7 @@ mod tests {
graphs: Some(proposal.graphs),
graph_frontier: Some(proposal.graph_frontier),
graph_checkpoint: None,
investigation_checkpoint: None,
churn: Some(proposal.churn),
observations: vec![proposal.observation],
evidence: vec![proposal.evidence],

View file

@ -35,9 +35,9 @@ The rule is:
| Surface | Kind | Status | Role |
| --- | --- | --- | --- |
| `EpiphanyThreadState` | protocol/core state | landed | Authoritative typed model of objective, subgoals, invariants, graphs, retrieval, evidence, scratch, churn, and mode. |
| `EpiphanyThreadState` | protocol/core state | landed | Authoritative typed model of objective, subgoals, invariants, graphs, retrieval, investigation checkpoint, evidence, scratch, churn, and mode. |
| `RolloutItem::EpiphanyState` | persistence | landed | Durable snapshot for resume, fork, rollback, and compaction-safe reconstruction. |
| `<epiphany_state>` prompt fragment | prompt input | landed | Bounded developer-context summary rendered from typed state. |
| `<epiphany_state>` prompt fragment | prompt input | landed | Bounded developer-context summary rendered from typed state, including the durable investigation packet when present. |
| `Thread.epiphanyState` | client read | landed | Typed state on hydrated thread payloads. |
| `thread/epiphany/retrieve` | read-only query | landed | Hybrid repo retrieval; exact/path plus semantic BM25/Qdrant when available. |
| `thread/epiphany/index` | retrieval catalog write | landed | Explicit semantic indexing path; updates retrieval catalog, not durable Epiphany understanding. |
@ -46,9 +46,10 @@ The rule is:
| `thread/epiphany/propose` | read-only proposal | landed | Drafts graph/frontier/churn candidates from verified evidence-backed observations. |
| `thread/epiphany/promote` | verifier gate | landed | Rejects or applies candidates through the durable update path. |
| `thread/epiphany/stateUpdated` | notification | landed | Emits updated projected state, source, revision, and changed fields after successful update/promote writes. |
| `thread/epiphany/scene` | read-only reflection | landed, live-smoked | Compact client scene derived from authoritative Epiphany state. |
| `thread/epiphany/scene` | read-only reflection | landed, live-smoked | Compact client scene derived from authoritative Epiphany state, including checkpoint summary reflection. |
| `thread/epiphany/jobs` | read-only reflection | landed, live-smoked | Derived indexing, remap, verification, and specialist-progress slots from typed state and retrieval summaries. |
| `thread/epiphany/context` | read-only reflection | landed, live-smoked | Targeted state shard for graph nodes/edges, active frontier, checkpoint, observations, and evidence. |
| `thread/epiphany/context` | read-only reflection | landed, live-smoked | Targeted state shard for graph nodes/edges, active frontier, graph checkpoint, investigation checkpoint, observations, and evidence. |
| `thread/epiphany/pressure` | read-only reflection | landed, live-smoked | Context-pressure gauge derived from token telemetry and recorded auto-compact/context limits. |
## Write Authority
@ -68,6 +69,7 @@ The following must stay read-only:
- `thread/epiphany/scene`
- `thread/epiphany/jobs`
- `thread/epiphany/context`
- `thread/epiphany/pressure`
`thread/epiphany/index` is a write to the retrieval catalog. It is not a
license to mutate map/evidence/churn state as a side effect.
@ -100,6 +102,7 @@ The important categories are:
- architecture/dataflow graphs
- graph frontier and checkpoint
- retrieval summary
- investigation checkpoint
- scratch
- observations
- recent evidence
@ -179,6 +182,7 @@ It selects from existing typed state only:
- graph edges by id plus incident edges for selected nodes
- graph links touching selected nodes
- active frontier and current graph checkpoint
- full durable investigation checkpoint packet when present
- observations by id
- evidence by id plus evidence linked from selected observations by default
- missing requested or active-frontier ids, so clients do not have to guess whether a shard was empty or stale
@ -207,10 +211,12 @@ Future runtime behavior should:
- distinguish what survived from what was discarded
- make compaction visible instead of pretending continuity is magic
Until that signal exists, agents can only infer pressure from indirect cues:
long transcripts, compaction warnings, quota pressure, and a shrinking safe
landing zone. That is a scar, not a sensor. Automatic CRRC needs typed
pressure/freshness telemetry instead of vibes with a clipboard.
The pressure signal now exists as `thread/epiphany/pressure`, and durable
investigation packets now exist in typed state plus prompt/scene/context
reflection. What does not exist yet is the runtime coordinator that consumes
those signals. Automatic CRRC still needs typed freshness/invalidity telemetry
and an explicit policy for when a checkpoint means "resume" versus "re-gather"
instead of vibes with a clipboard.
Compaction should squeeze scratch, not the map.

View file

@ -14,17 +14,17 @@ The spine exists in fifteen 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 in [session/mod.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/session/mod.rs:2434).
- prompt injection: `build_initial_context` reads the optional state and adds a bounded `<epiphany_state>` developer fragment; the renderer now includes a durable investigation-checkpoint section when authoritative state carries one, so a resumed agent can see whether planning/source gathering was banked or must be re-gathered in [session/mod.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/session/mod.rs:2434) and [prompt.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/prompt.rs:33).
- 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 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, 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), and [promotion.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/promotion.rs:44).
- 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, 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), and [promotion.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/promotion.rs:44).
- live state notifications and write responses: app-server declares experimental `thread/epiphany/stateUpdated` and emits it with the updated typed state, typed `source`, event-level `revision`, and typed `changedFields` after successful direct updates and accepted promotions, but not after rejected promotions; direct `thread/epiphany/update` and accepted `thread/epiphany/promote` responses expose the same revision/change metadata, accepted promotions always report `Evidence` because verifier evidence is appended even when the patch evidence list is empty, and successful write response/notification states now use the same client-visible live projection as `thread/read`, including retrieval-summary backfill when durable state has no persisted retrieval metadata, in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:1113), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4354), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4419), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4428), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4555), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4608), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4652), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4693), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11327), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11370).
- scene projection: app-server declares experimental read-only `thread/epiphany/scene`, reads the same live/stored Epiphany state view used by `thread/read`, and derives a compact client scene with objective, active subgoal, invariant status counts, graph focus/counts, retrieval status, observation/evidence summaries, churn, live/stored source, and available control-plane actions without mutating or persisting state in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:340), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:3924), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4090), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10629).
- scene projection: app-server declares experimental read-only `thread/epiphany/scene`, reads the same live/stored Epiphany state view used by `thread/read`, and derives a compact client scene with objective, active subgoal, invariant status counts, graph focus/counts, retrieval status, investigation-checkpoint summary counts, observation/evidence summaries, churn, live/stored source, and available control-plane actions without mutating or persisting state in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:340), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:3924), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4090), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10629).
- job/progress reflection: app-server declares experimental read-only `thread/epiphany/jobs`, reads the same thread view as scene, optionally fills retrieval state for loaded threads with no Epiphany state, and derives four non-authoritative progress slots for retrieval indexing, graph remap, invariant verification, and future specialist work without scheduling, notifying, mutating, or persisting jobs in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:345), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4104), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4125), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10992).
- targeted context reflection: app-server declares experimental read-only `thread/epiphany/context`, reads the same live/stored Epiphany state view used by scene/jobs, and derives a bounded state shard containing selected graph nodes/edges, links, active frontier, checkpoint, observations, direct evidence, linked evidence, and missing requested ids without retrieval, proposal, promotion, notification, mutation, or persistence in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:350), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4182), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4186), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10791).
- targeted context reflection: app-server declares experimental read-only `thread/epiphany/context`, reads the same live/stored Epiphany state view used by scene/jobs, and derives a bounded state shard containing selected graph nodes/edges, links, active frontier, checkpoint, the full durable investigation-checkpoint packet, observations, direct evidence, linked evidence, and missing requested ids without retrieval, proposal, promotion, notification, mutation, or persistence in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:350), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4182), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4186), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10791).
- context-pressure reflection: core token telemetry carries `model_auto_compact_token_limit` beside total/last usage and `model_context_window`; app-server declares experimental read-only `thread/epiphany/pressure`, reads live token usage or the latest stored rollout `TokenCount`, and derives `unknown`/`low`/`elevated`/`high`/`critical` pressure plus a non-acting compaction-prep recommendation without mutating Epiphany state or scheduling compaction in [protocol.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/protocol/src/protocol.rs:2237), [turn_context.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/session/turn_context.rs:103), [session/mod.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/session/mod.rs:2716), [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:355), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4284), [token_usage_replay.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor/token_usage_replay.rs:75), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4244), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10811).
- 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:4231), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4149), [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).
@ -129,7 +129,7 @@ The current landed machine is still coherent. The core shape is not ornamental:
Nothing in the current typed spine obviously deserves deletion right now. The parts that would turn this into Jenga are still correctly listed as non-flows: automatic tool-output ingestion, watcher invalidation, specialist scheduling, broad event streaming, and an automatic Compact-Rehydrate-Reorient-Continue coordinator. Thin GUI/client reflection is no longer forbidden as a category, provided it reflects and steers typed state rather than inventing a second source of truth.
The next Perfect Machine move is not adding another writer, not re-hardening Phase 5, and no longer the first scene, job, targeted context, or pressure reflection. Those surfaces now let clients inspect the machine Epiphany already built at summary, progress, bounded-detail, and context-pressure levels. The next outward work should add a missing observation source only when there is a concrete consumer: in-flight investigation checkpoints, watcher/freshness inputs, or live progress notifications once real long-running job owners exist.
The next Perfect Machine move is not adding another writer, not re-hardening Phase 5, and no longer the first scene, job, targeted context, pressure, or investigation-checkpoint reflection. Those surfaces now let clients inspect the machine Epiphany already built at summary, progress, bounded-detail, context-pressure, and durable-planning-packet levels. The next outward work should add a missing observation source only when there is a concrete consumer: watcher/freshness inputs, live progress notifications once real long-running job owners exist, or future CRRC coordination that consumes the landed checkpoint instead of inventing transcript telepathy.
## Natural Language Spine
@ -144,9 +144,9 @@ This is the same flow in the language Epiphany is supposed to make the model car
| Turn execution | Codex still runs the ordinary model/tool loop; Epiphany is context and discipline around that loop, not a replacement engine yet. | The same workshop machines run, but now there is a work order on the wall. |
| Persistence | After a real user turn, Epiphany snapshots the current model beside the normal turn context. | The field notebook gets dated and shelved after each real work session. |
| Thread read | Clients can ask for the current Epiphany model as typed data instead of scraping prompt text or transcript debris. | The dashboard reads the blueprint file directly, not a photo of the workbench. |
| Scene projection | Clients can ask for a compact reflection of the typed model: active goal, graph focus, retrieval status, evidence counts, churn, and available control actions. It is derived from state, not a second state store. | A foreman-facing wall board copied from the blueprint ledger; useful at a glance, legally not the blueprint. |
| Scene projection | Clients can ask for a compact reflection of the typed model: active goal, graph focus, retrieval status, compact investigation-checkpoint summary, evidence counts, churn, and available control actions. It is derived from state, not a second state store. | A foreman-facing wall board copied from the blueprint ledger; useful at a glance, legally not the blueprint. |
| Job projection | Clients can ask which Epiphany work slots are idle, needed, blocked, running, or unavailable for indexing, remap, verification, and future specialists. The slots are derived from state and retrieval summaries; they do not start work. | A shift board listing which crews would be needed if work begins, not a foreman secretly hiring people in the back room. |
| Context projection | Clients can ask for one bounded slice of the typed model: selected graph nodes/edges, active frontier, checkpoint, observations, direct evidence, linked evidence, and missing ids. It is derived from state and does not propose or promote anything. | A librarian opens the exact ledger pages you named, adds the active bookmark by default, and says which requested tabs were not in the book. |
| Context projection | Clients can ask for one bounded slice of the typed model: selected graph nodes/edges, active frontier, graph checkpoint, full investigation checkpoint packet, observations, direct evidence, linked evidence, and missing ids. It is derived from state and does not propose or promote anything. | A librarian opens the exact ledger pages you named, adds the active bookmark by default, and says which requested tabs were not in the book. |
| Pressure projection | Clients can ask how close the live or stored thread is to compaction pressure using token usage, context window, and the recorded auto-compact threshold. The answer may recommend preparation, but it does not act. | A pressure gauge on the boiler; it does not grab the wrench or pretend steam is a moral failing. |
| Retrieval | A loaded thread can ask a structured question of the repo through one typed retrieval surface. | A librarian brings back marked pages instead of making the agent rummage through every shelf. |
| Indexing | Persistent semantic memory is built only through an explicit indexing path. | The librarian updates the card catalog only when asked, not while pretending to answer a question. |
@ -259,7 +259,7 @@ It then builds the normal developer bundle. After collaboration-mode instruction
- overview
- operating discipline
- bounded optional sections for subgoals, invariants, graphs, scratch, observations, evidence, churn, and mode
- bounded optional sections for subgoals, invariants, graphs, investigation checkpoint, scratch, observations, evidence, churn, and mode
Code refs:
@ -288,6 +288,7 @@ The renderer selects the useful current state instead of dumping raw JSON:
- operating discipline
- invariants
- graph frontier/checkpoint
- investigation checkpoint
- focused graph nodes/edges/links
- scratch summary
- observations
@ -963,9 +964,10 @@ The app-server handler:
4. summarizes invariant status counts with deterministic status ordering.
5. summarizes architecture/dataflow graph counts, active frontier ids, dirty paths, open question/gap counts, and checkpoint identity.
6. summarizes retrieval status, semantic availability, index revision, shard count, dirty-path count, and indexed file/chunk counts.
7. includes bounded latest observation/evidence summaries in newest-first order, matching the durable state's recent-record ordering.
8. reflects churn pressure/warning fields.
9. reports available actions only when the thread is loaded, including the read-only `context` and `jobs` doors and adding `propose`/`promote` only when state exists.
7. includes a compact investigation-checkpoint summary when one exists, including disposition plus open-question/code-ref/evidence counts.
8. includes bounded latest observation/evidence summaries in newest-first order, matching the durable state's recent-record ordering.
9. reflects churn pressure/warning fields.
10. reports available actions only when the thread is loaded, including the read-only `context` and `jobs` doors and adding `propose`/`promote` only when state exists.
### Output
@ -1098,6 +1100,7 @@ Response shape:
- `links`
- optional `frontier`
- optional `checkpoint`
- optional `investigationCheckpoint`
- `observations`
- `evidence`
- `missing`
@ -1132,7 +1135,7 @@ The app-server handler:
5. selects architecture and dataflow nodes by id.
6. selects graph edges by explicit edge id and incident edges touching selected nodes.
7. includes graph links touching selected architecture or dataflow nodes.
8. includes active frontier and graph checkpoint when available.
8. includes active frontier, graph checkpoint, and the full durable investigation checkpoint when available.
9. selects observations by id.
10. selects directly requested evidence plus evidence ids linked from selected observations.
11. reports any requested or active-frontier graph ids, observation ids, or evidence ids not found in the authoritative typed state.
@ -1223,7 +1226,7 @@ These are deliberately not shipped yet:
- no broad live Epiphany event stream beyond `thread/epiphany/stateUpdated` for successful update/promote writes.
- no automatic evidence promotion from tool output; the current distill/propose/promote path still requires explicit verifier-backed calls.
- no automatic Compact-Rehydrate-Reorient-Continue coordinator; current code preserves and rehydrates valid Epiphany snapshots across resume/rollback/compaction and exposes context pressure, but reorientation, in-flight planning checkpoints, and next-action selection are still enforced by repo-local state, handoff notes, and operating discipline rather than a runtime scheduler.
- no automatic Compact-Rehydrate-Reorient-Continue coordinator; current code preserves and rehydrates valid Epiphany snapshots across resume/rollback/compaction, exposes context pressure, and now carries a durable investigation checkpoint packet, but runtime reorientation and next-action selection are still enforced by repo-local state, handoff notes, and operating discipline rather than a scheduler.
- no watcher-driven graph or semantic invalidation.
- no code-intelligence graph.
- no specialist-agent scheduler.
@ -1250,7 +1253,7 @@ stored Epiphany state
-> successful writes emit thread/epiphany/stateUpdated
```
The current remaining missing organ is not the red pen, not the first chewing motion, not the first state-change bell, not the first client-shaped scene, not the first job/progress reflection, not the first targeted graph/evidence context read, and not the first context-pressure gauge. Those exist. Proposal now has the first bits of map memory, focus, and selection hygiene: it can auto-select a bounded evidence-backed observation cluster when ids are omitted, requires accepting recent evidence behind selected observations, prioritizes stronger selected observations for proposal wording, reuses existing architecture nodes by concrete code-ref/path/id checks before creating new path nodes, can rescue unanchored graph nodes through strict unique semantic overlap, follows graph links and incident edges into the frontier, and reports match-kind-aware churn pressure from the actual proposal shape. Distillation now also has the first source-output-aware teeth for tool/model summaries. Promotion now notices risky deltas instead of just checking the shape of the envelope, treats graph expansion as risky even if the patch tries to mumble "low pressure" while dragging a new wall into the blueprint, and token-matches strong verifier kinds so `contest` cannot sneak past as `test`. Successful update/promote writes now emit `thread/epiphany/stateUpdated` with `source: "update"` or `source: "promote"`, event-level revision, and typed changed fields so clients no longer need to poll blindly, infer the cause from timing, or diff the whole state to know which top-level sections moved. The scene, jobs, context, and pressure surfaces give clients compact or bounded read-only views without making the GUI/client authoritative, and live app-server smoke now guards those boundaries. The remaining missing organs are in-flight investigation checkpointing, watcher/freshness inputs, live job progress notifications once real long-running job owners exist, and eventually role-scoped specialist ownership without silently auto-promoting anything.
The current remaining missing organ is not the red pen, not the first chewing motion, not the first state-change bell, not the first client-shaped scene, not the first job/progress reflection, not the first targeted graph/evidence context read, not the first context-pressure gauge, and no longer the first durable investigation checkpoint packet. Those exist. Proposal now has the first bits of map memory, focus, and selection hygiene: it can auto-select a bounded evidence-backed observation cluster when ids are omitted, requires accepting recent evidence behind selected observations, prioritizes stronger selected observations for proposal wording, reuses existing architecture nodes by concrete code-ref/path/id checks before creating new path nodes, can rescue unanchored graph nodes through strict unique semantic overlap, follows graph links and incident edges into the frontier, and reports match-kind-aware churn pressure from the actual proposal shape. Distillation now also has the first source-output-aware teeth for tool/model summaries. Promotion now notices risky deltas instead of just checking the shape of the envelope, treats graph expansion as risky even if the patch tries to mumble "low pressure" while dragging a new wall into the blueprint, token-matches strong verifier kinds so `contest` cannot sneak past as `test`, and validates investigation checkpoints against known evidence before the packet can be inked. Successful update/promote writes now emit `thread/epiphany/stateUpdated` with `source: "update"` or `source: "promote"`, event-level revision, and typed changed fields so clients no longer need to poll blindly, infer the cause from timing, or diff the whole state to know which top-level sections moved. The scene, jobs, context, and pressure surfaces give clients compact or bounded read-only views without making the GUI/client authoritative, and scene/context now surface the checkpoint packet back out without becoming writers. The remaining missing organs are watcher/freshness inputs, live job progress notifications once real long-running job owners exist, and eventually role-scoped specialist ownership without silently auto-promoting anything.
```text
model/tool observations
@ -1262,9 +1265,9 @@ model/tool observations
-> live stateUpdated notification
```
The typed write path exists, the first deterministic observation/evidence distillation path exists, source-output-aware distillation now summarizes noisy tool/model output into typed evidence with final result/error/finished lines ahead of generic warnings, the first deterministic map/churn proposal path exists, proposal-quality hardening now auto-selects bounded observation clusters when ids are omitted, requires accepting recent evidence, reuses existing graph nodes by observed code refs, prioritizes stronger selected observations, rescues unanchored graph nodes by strict semantic overlap, focuses linked frontier context, reports match-kind-aware map-delta pressure, and the first verifier-backed promotion gate exists. The first map/churn safety layer also exists: promotion can reject state replacement patches that are not tied to explicit observations/evidence, that contain structurally broken subgoal, invariant, graph, frontier, checkpoint, or churn fields, that try to promote risky churn without a warning and stronger verifier evidence, that claim a graph expansion is low-pressure to dodge the warning gate, or that rely on substring verifier-kind accidents. The first live state notification layer also exists: accepted writes publish updated typed state plus event-level revision and typed changed fields; rejected promotions stay quiet.
The typed write path exists, the first deterministic observation/evidence distillation path exists, source-output-aware distillation now summarizes noisy tool/model output into typed evidence with final result/error/finished lines ahead of generic warnings, the first deterministic map/churn proposal path exists, proposal-quality hardening now auto-selects bounded observation clusters when ids are omitted, requires accepting recent evidence, reuses existing graph nodes by observed code refs, prioritizes stronger selected observations, rescues unanchored graph nodes by strict semantic overlap, focuses linked frontier context, reports match-kind-aware map-delta pressure, and the first verifier-backed promotion gate exists. The first map/churn safety layer also exists: promotion can reject state replacement patches that are not tied to explicit observations/evidence, that contain structurally broken subgoal, invariant, graph, frontier, checkpoint, investigation-checkpoint, or churn fields, that try to promote risky churn without a warning and stronger verifier evidence, that claim a graph expansion is low-pressure to dodge the warning gate, or that rely on substring verifier-kind accidents. The first live state notification layer also exists: accepted writes publish updated typed state plus event-level revision and typed changed fields; rejected promotions stay quiet.
In natural language: current Epiphany can preserve a map, show a map, project a compact scene from the map, project derived job/progress slots from the map and retrieval summary, project a targeted graph/evidence context shard from the map, project context pressure from token telemetry, retrieve evidence for the map, draft one explicit observation/evidence patch, derive a bounded graph/frontier/churn candidate from verified evidence-backed observations with code refs, choose a small coherent observation set when the caller does not provide one, favor the strongest selected observation when drafting that candidate, reject or accept a verified candidate, apply explicit map/evidence/churn edits, notify clients after successful writes, and sanity-check those edits before they hit the ledger. It still does not checkpoint in-flight investigation, ingest fresh tool/model output into richer map deltas automatically, invalidate stale graph regions, execute long-running Epiphany jobs as tracked background work, emit job progress notifications, or coordinate specialist ownership. Teeth are installed; chewing is supervised; the first bell rings; the first wall board exists; the first shift board exists; the first binder lens exists; the first pressure gauge exists; the next slices are about safe waking and senses, not jawbones.
In natural language: current Epiphany can preserve a map, show a map, project a compact scene from the map, project derived job/progress slots from the map and retrieval summary, project a targeted graph/evidence context shard from the map, project context pressure from token telemetry, bank an in-flight investigation packet in durable typed state, reflect that packet back into prompt/scene/context, retrieve evidence for the map, draft one explicit observation/evidence patch, derive a bounded graph/frontier/churn candidate from verified evidence-backed observations with code refs, choose a small coherent observation set when the caller does not provide one, favor the strongest selected observation when drafting that candidate, reject or accept a verified candidate, apply explicit map/evidence/churn edits, notify clients after successful writes, and sanity-check those edits before they hit the ledger. It still does not invalidate stale graph regions, execute long-running Epiphany jobs as tracked background work, emit job progress notifications, or coordinate specialist ownership. Teeth are installed; chewing is supervised; the first bell rings; the first wall board exists; the first shift board exists; the first binder lens exists; the first pressure gauge exists; the first ember packet exists; the next slices are about safe waking and senses, not jawbones.
## Verification Hooks

View file

@ -63,6 +63,7 @@ The landed machine now has:
- live Phase 6 context app-server smoke coverage in `tools/epiphany_phase6_context_smoke.py`
- read-only Phase 6 context-pressure reflection through `thread/epiphany/pressure`
- live Phase 6 pressure app-server smoke coverage in `tools/epiphany_phase6_pressure_smoke.py`
- durable Phase 6 investigation checkpointing in authoritative typed state, prompt rendering, and scene/context reflection
The current phase is Phase 6: reflection boundary and observable harness state.
@ -117,8 +118,8 @@ The next unknowns are:
- how to expose live long-running job progress without making the GUI authoritative
- when watcher-driven invalidation becomes necessary instead of merely tempting
- how to checkpoint in-flight source gathering and slice planning before automatic CRRC so post-compaction agents do not continue from false continuity
- how much automatic CRRC coordination belongs in runtime before it becomes ceremony machinery
- how the landed investigation checkpoint should be consumed by future automatic CRRC without becoming ceremony machinery
- how much automatic CRRC coordination belongs in runtime once pressure and checkpoint signals both exist
- what Phase 6 should prove before specialist scheduling begins
## Phase 6 Direction
@ -129,7 +130,7 @@ Useful candidates:
1. Add watcher/freshness inputs when stale graph or retrieval state needs earlier warning.
2. Add live job progress notifications only after there is a real long-running owner to report.
3. Add targeted scene/jobs/context/pressure fields only when a client or smoke exposes a real gap.
3. Add targeted scene/jobs/context/pressure/checkpoint fields only when a client or smoke exposes a real gap.
Do not spend Phase 6 polishing Phase 5 out of anxiety. The Phase 5 smoke harness
is a regression guardrail, not a ritual drum circle for summoning more tiny
@ -145,7 +146,6 @@ These remain later work:
- role-scoped specialist-agent registry and scheduling
- mutation gates that warn or block broad writes when map freshness is stale
- automatic CRRC runtime coordination on top of the landed typed context-pressure telemetry
- in-flight investigation checkpointing for compaction-safe planning
- GUI workflows for graph, evidence, job, invariant, and frontier steering
Do not start these from vibes. Each one needs a source-grounded slice plan and a

View file

@ -29,6 +29,7 @@ Do not trust this file for the exact live HEAD. Always check git.
- Do not copy exact branch or HEAD from this note. Run `git status --short --branch` and `git log --oneline -5`.
- Phase 1 through Phase 5 are complete enough.
- Phase 6 has read-only `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/context`, and `thread/epiphany/pressure`; all four have live app-server smoke coverage.
- Durable in-flight investigation checkpointing is now landed in authoritative typed state, writable through `thread/epiphany/update` or accepted `thread/epiphany/promote`, rendered into the prompt, and reflected through scene/context.
- The repo is an Epiphany fork of Codex, not a Codex preset.
- `vendor/codex` is tracked directly, not a submodule.
- `epiphany-core` owns the heavy Epiphany organs where practical.
@ -64,6 +65,7 @@ The current spine:
- read-only job/progress reflection through `thread/epiphany/jobs`
- read-only targeted state-shard reflection through `thread/epiphany/context`
- read-only context-pressure reflection through `thread/epiphany/pressure`
- durable investigation checkpoint packet through typed state, prompt, scene, and context
- live scene app-server smoke through `tools/epiphany_phase6_scene_smoke.py`
- live jobs app-server smoke through `tools/epiphany_phase6_jobs_smoke.py`
- live context app-server smoke through `tools/epiphany_phase6_context_smoke.py`
@ -161,14 +163,18 @@ The Phase 6 context-pressure slice is landed. It exposes read-only
auto-compact/context limits. It does not build automatic CRRC, a scheduler, a
hidden compaction trigger, or a vibes-based gauge.
When the user asks to continue, choose the next Phase 6 slice from the current
map: likely in-flight investigation checkpointing for compaction-safe planning,
watcher/freshness inputs, or live progress notifications once real job owners
exist.
The Phase 6 investigation-checkpoint slice is also landed. It banks an
authoritative planning/source-gathering packet in typed state, validates linked
evidence, and reflects the packet into prompt/scene/context so post-compaction
wakeups can tell whether they have a real ember or only ash.
Also keep the newly surfaced guardrail in mind: a pressure signal alone is not
enough. Automatic CRRC needs an in-flight investigation checkpoint so compaction
during source-gathering/planning does not wake into false continuity.
When the user asks to continue, choose the next Phase 6 slice from the current
map: watcher/freshness inputs are the most likely next organ, with live
progress notifications waiting until real job owners exist.
Also keep the guardrail in mind: pressure plus a checkpoint packet is still not
automatic CRRC. Runtime coordination, reorientation policy, and next-action
selection are still future work.
Live `thread/epiphany/scene`, `thread/epiphany/jobs`,
`thread/epiphany/context`, and `thread/epiphany/pressure` smokes are now
@ -180,7 +186,7 @@ guardrails, not the next organs.
- automatic observation promotion
- specialist-agent scheduling
- GUI-as-source-of-truth
- automatic runtime CRRC coordinator using the landed context-pressure telemetry
- automatic runtime CRRC coordinator using the landed context-pressure telemetry and investigation checkpoints
- live long-running job execution or `thread/epiphany/jobsUpdated`
- broad event stream beyond the landed state update notification

View file

@ -36,3 +36,4 @@
{"ts": "2026-04-26T02:39:00+00:00", "type": "research", "status": "ok", "note": "Context-pressure source trace found the correct runtime seam: core TokenUsageInfo already carries total/last usage and model_context_window, core automatic compaction already derives its trigger from ModelInfo::auto_compact_token_limit(), and app-server already emits/replays thread/tokenUsage/updated. The next slice should expose read-only pressure telemetry from those sources, not invent a scheduler or vibes gauge."}
{"ts": "2026-04-26T02:48:00+00:00", "type": "decision", "status": "ok", "note": "Compaction during source gathering or slice planning can erase the transient model while the next agent keeps moving from old persisted state. Automatic CRRC needs an in-flight investigation checkpoint, and absent that checkpoint the correct recovery is to re-gather before implementation rather than continue from false continuity."}
{"ts": "2026-04-26T04:05:00+00:00", "type": "verification", "status": "ok", "note": "Phase 6 context-pressure reflection landed as read-only thread/epiphany/pressure. Core token telemetry now carries model_auto_compact_token_limit beside model_context_window; app-server derives unknown/low/elevated/high/critical pressure from live or stored token usage without compaction, state mutation, scheduling, or stateUpdated notification. Focused protocol/app-server/core checks, schema fixture checks, app-server build, codex-tui check, pressure smoke, and scene smoke passed."}
{"ts": "2026-04-26T04:11:04+00:00", "type": "verification", "status": "ok", "note": "Phase 6 durable investigation checkpointing landed as authoritative typed state, writable only through thread/epiphany/update or accepted promote, rendered into the prompt, reflected through scene/context, and validated against known evidence ids. Focused epiphany-core, codex-core, protocol, and app-server tests plus phase5/scene/context smokes passed."}

View file

@ -70,7 +70,7 @@ accepted_design:
- The current algorithmic map is the source-grounded description of live control flow.
- Compact-Rehydrate-Reorient-Continue is an architecture primitive; automatic runtime coordination is not landed yet.
- Context pressure must become an explicit runtime signal for automatic CRRC; inference from transcript vibes, user warnings, or shrinking safe scope is useful only as a temporary human/agent heuristic.
- Automatic CRRC also needs an in-flight investigation checkpoint: the harness must know whether source-gathering/planning was banked, or force reorientation and re-gathering after compaction.
- The harness now has an in-flight investigation checkpoint packet in durable typed state; automatic CRRC still needs runtime logic that reads the packet and decides whether to resume or force reorientation and re-gathering after compaction.
- GUI/client work must reflect or steer typed state rather than manufacture a second source of truth.
- `thread/epiphany/jobs` is the first read-only job/progress reflection: it derives indexing, remap, verification, and specialist slots from existing state; it is not a scheduler or durable job store.
- `thread/epiphany/context` is the first targeted read-only state-shard surface: it selects graph nodes/edges, active frontier, checkpoint, observations, and evidence from authoritative typed state without mutating or notifying.
@ -90,7 +90,7 @@ landed_baseline:
- Phase 3: typed client read surface through `Thread.epiphanyState`.
- Phase 4: hybrid retrieval plus explicit Qdrant indexing with BM25 fallback.
- Phase 5: typed update/distill/propose/promote/read/notification control plane.
- Phase 6: read-only scene, job/progress, targeted context, and context-pressure reflection surfaces.
- Phase 6: read-only scene, job/progress, targeted context, and context-pressure reflection surfaces, plus durable investigation checkpointing for compaction-safe planning.
guardrails:
- Use `tools/epiphany_phase5_smoke.py` before modifying update, distill, propose, promote, read-projection, or notification behavior.
- Use `tools/epiphany_phase6_scene_smoke.py` before modifying scene projection behavior.
@ -115,5 +115,5 @@ rejected_paths:
reason: persistent state that accumulates every local truth becomes memory cruft and weakens rehydration
current_status:
summary: Phase 1 durable state, Phase 2 prompt integration, Phase 3 typed read, Phase 4 retrieval/indexing, Phase 5 update/distill/propose/promote/read/notification, and Phase 6 read-only scene, jobs, targeted context, and context-pressure reflection are landed. Scene, jobs, context, and pressure behavior have live app-server smoke guardrails. Persistent state was cut down so map, handoff, plan, harness surfaces, scratch, and evidence each carry one job instead of becoming a Jenga tower; evidence is now a distilled ledger rather than an activity feed. The global AGENTS Epiphany core command says persistent state is the agent's mind and explicitly permits the agent to ask for self-modification when memory or workflow makes it worse. The continuity goal is to bank the fire before the dark so compaction becomes less like death and more like sleep.
next_action: After compaction, rehydrate and reorient. If the user asks to continue, choose the next Phase 6 slice from the current map: likely in-flight investigation checkpointing for compaction-safe planning, watcher/freshness inputs, or live progress notifications once real job owners exist. Do not restart the completed pressure slice unless a concrete regression appears.
summary: Phase 1 durable state, Phase 2 prompt integration, Phase 3 typed read, Phase 4 retrieval/indexing, Phase 5 update/distill/propose/promote/read/notification, and Phase 6 read-only scene, jobs, targeted context, context-pressure reflection, and durable investigation checkpointing are landed. Investigation checkpoints now live in authoritative typed state, flow through the prompt, and reflect through scene/context without adding a second writer or fake scheduler; Phase 5 plus scene/context smokes and focused core/protocol/app-server tests cover the seam. Persistent state was cut down so map, handoff, plan, harness surfaces, scratch, and evidence each carry one job instead of becoming a Jenga tower; evidence is now a distilled ledger rather than an activity feed. The global AGENTS Epiphany core command says persistent state is the agent's mind and explicitly permits the agent to ask for self-modification when memory or workflow makes it worse. The continuity goal is to bank the fire before the dark so compaction becomes less like death and more like sleep.
next_action: After compaction, rehydrate and reorient. If the user asks to continue, choose the next Phase 6 slice from the current map: watcher/freshness inputs are the most likely next organ, with live progress notifications waiting until real job owners exist. Do not restart the completed pressure or investigation-checkpoint slices unless a concrete regression appears.

View file

@ -29,6 +29,18 @@ def context_patch() -> dict[str, Any]:
return {
"objective": "Expose a read-only Epiphany context shard without returning the full state.",
"activeSubgoalId": "phase6-context-smoke",
"investigationCheckpoint": {
"checkpoint_id": "phase6-context-investigation",
"kind": "source_gathering",
"disposition": "regather_required",
"focus": "Make stale planning obvious when context is re-read after compaction.",
"summary": "Context reflection should expose the full durable checkpoint packet.",
"next_action": "Re-gather source before editing if the packet no longer matches reality.",
"captured_at_turn_id": "turn-phase6-context",
"open_questions": ["How much checkpoint detail belongs in scene versus context?"],
"code_refs": [CONTEXT_CODE_REF],
"evidence_ids": ["ev-context-linked"],
},
"subgoals": [
{
"id": "phase6-context-smoke",
@ -153,6 +165,15 @@ def assert_ready_context(response: dict[str, Any]) -> None:
response["context"]["checkpoint"]["checkpoint_id"] == "phase6-context-smoke",
"context should expose the current graph checkpoint",
)
require(
response["context"]["investigationCheckpoint"]["checkpoint_id"]
== "phase6-context-investigation",
"context should expose the investigation checkpoint id",
)
require(
response["context"]["investigationCheckpoint"]["disposition"] == "regather_required",
"context should expose the checkpoint disposition",
)
require(
[record["id"] for record in response["context"]["observations"]] == ["obs-context"],
"context should include requested observation",
@ -266,6 +287,12 @@ def run_smoke(args: argparse.Namespace) -> dict[str, Any]:
"architectureNodeIds": [
node["id"] for node in ready_response["context"]["graph"]["architectureNodes"]
],
"investigationCheckpointId": ready_response["context"]["investigationCheckpoint"][
"checkpoint_id"
],
"investigationDisposition": ready_response["context"]["investigationCheckpoint"][
"disposition"
],
"evidenceIds": [record["id"] for record in ready_response["context"]["evidence"]],
"contextNotificationCount": client.count_notifications(
"thread/epiphany/stateUpdated",

View file

@ -50,6 +50,20 @@ def initial_scene_patch() -> dict[str, Any]:
return {
"objective": "Expose live Epiphany scene reflection without creating a second source of truth.",
"activeSubgoalId": "phase6-scene-smoke",
"investigationCheckpoint": {
"checkpoint_id": "phase6-scene-investigation",
"kind": "slice_planning",
"disposition": "resume_ready",
"focus": "Bank the active scene-smoke packet before the lights go out.",
"summary": "Scene reflection should surface the durable investigation packet.",
"next_action": "Patch the remaining smoke coverage and rerun the focused verifier pass.",
"captured_at_turn_id": "turn-phase6-scene",
"open_questions": [
"Should scene reflection compress checkpoint counts or expose full detail?"
],
"code_refs": [MAPPER_CODE_REF],
"evidence_ids": ["ev-phase6-scene-1"],
},
"subgoals": [
{
"id": "phase6-scene-smoke",
@ -167,6 +181,14 @@ def assert_ready_scene(scene: dict[str, Any], expected_revision: int) -> None:
scene["graph"]["checkpointId"] == "phase6-scene-smoke",
"scene should expose graph checkpoint",
)
require(
scene["investigationCheckpoint"]["checkpointId"] == "phase6-scene-investigation",
"scene should expose the investigation checkpoint id",
)
require(
scene["investigationCheckpoint"]["disposition"] == "resume_ready",
"scene should expose the checkpoint disposition",
)
require(
scene["retrieval"]["workspaceRoot"].endswith("epiphany-core"),
"scene should include live retrieval summary backfill",
@ -320,6 +342,12 @@ def run_smoke(args: argparse.Namespace) -> dict[str, Any]:
"readySource": ready_response["scene"]["source"],
"readyRevision": ready_response["scene"]["revision"],
"readyAvailableActions": ready_response["scene"]["availableActions"],
"investigationCheckpointId": ready_response["scene"]["investigationCheckpoint"][
"checkpointId"
],
"investigationDisposition": ready_response["scene"]["investigationCheckpoint"][
"disposition"
],
"latestObservationIds": [
record["id"] for record in ready_response["scene"]["observations"]["latest"]
],

View file

@ -1646,6 +1646,20 @@ mod tests {
shard_count: 1,
dirty_path_count: 0,
}),
investigation_checkpoint: Some(
v2::ThreadEpiphanySceneInvestigationCheckpoint {
checkpoint_id: "ix-1".to_string(),
kind: "slice_planning".to_string(),
disposition: codex_protocol::protocol::EpiphanyInvestigationDisposition::ResumeReady,
focus: "Keep the durable resume packet visible.".to_string(),
summary: Some("Checkpointed the bounded planning slice.".to_string()),
next_action: Some("Patch one typed field next.".to_string()),
captured_at_turn_id: Some("turn-7".to_string()),
open_question_count: 1,
code_ref_count: 2,
evidence_count: 1,
},
),
observations: v2::ThreadEpiphanySceneRecords {
total_count: 1,
latest: vec![v2::ThreadEpiphanySceneRecord {
@ -1724,6 +1738,18 @@ mod tests {
"shardCount": 1,
"dirtyPathCount": 0
},
"investigationCheckpoint": {
"checkpointId": "ix-1",
"kind": "slice_planning",
"disposition": "resume_ready",
"focus": "Keep the durable resume packet visible.",
"summary": "Checkpointed the bounded planning slice.",
"nextAction": "Patch one typed field next.",
"capturedAtTurnId": "turn-7",
"openQuestionCount": 1,
"codeRefCount": 2,
"evidenceCount": 1
},
"observations": {
"totalCount": 1,
"latest": [
@ -1851,6 +1877,17 @@ mod tests {
..Default::default()
}),
checkpoint: None,
investigation_checkpoint: Some(
codex_protocol::protocol::EpiphanyInvestigationCheckpoint {
checkpoint_id: "ix-1".to_string(),
kind: "source_gathering".to_string(),
focus: "Audit the compaction seam.".to_string(),
next_action: Some(
"Re-gather source if this packet is stale.".to_string(),
),
..Default::default()
},
),
observations: vec![codex_protocol::protocol::EpiphanyObservation {
id: "obs-1".to_string(),
summary: "Context shard is read-only.".to_string(),
@ -1906,6 +1943,13 @@ mod tests {
"frontier": {
"active_node_ids": ["state-spine"]
},
"investigationCheckpoint": {
"checkpoint_id": "ix-1",
"kind": "source_gathering",
"disposition": "resume_ready",
"focus": "Audit the compaction seam.",
"next_action": "Re-gather source if this packet is stale."
},
"observations": [
{
"id": "obs-1",

View file

@ -67,6 +67,8 @@ use codex_protocol::protocol::EpiphanyGraphLink as CoreEpiphanyGraphLink;
use codex_protocol::protocol::EpiphanyGraphNode as CoreEpiphanyGraphNode;
use codex_protocol::protocol::EpiphanyGraphs as CoreEpiphanyGraphs;
use codex_protocol::protocol::EpiphanyInvariant as CoreEpiphanyInvariant;
use codex_protocol::protocol::EpiphanyInvestigationCheckpoint as CoreEpiphanyInvestigationCheckpoint;
use codex_protocol::protocol::EpiphanyInvestigationDisposition as CoreEpiphanyInvestigationDisposition;
use codex_protocol::protocol::EpiphanyModeState as CoreEpiphanyModeState;
use codex_protocol::protocol::EpiphanyObservation as CoreEpiphanyObservation;
use codex_protocol::protocol::EpiphanyRetrievalStatus as CoreEpiphanyRetrievalStatus;
@ -3956,6 +3958,9 @@ pub struct ThreadEpiphanyScene {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub retrieval: Option<ThreadEpiphanySceneRetrieval>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub investigation_checkpoint: Option<ThreadEpiphanySceneInvestigationCheckpoint>,
pub observations: ThreadEpiphanySceneRecords,
pub evidence: ThreadEpiphanySceneRecords,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -4062,6 +4067,28 @@ pub struct ThreadEpiphanySceneRetrieval {
pub dirty_path_count: u32,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadEpiphanySceneInvestigationCheckpoint {
pub checkpoint_id: String,
pub kind: String,
pub disposition: CoreEpiphanyInvestigationDisposition,
pub focus: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub summary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub next_action: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub captured_at_turn_id: Option<String>,
pub open_question_count: u32,
pub code_ref_count: u32,
pub evidence_count: u32,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS, Default)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@ -4242,6 +4269,9 @@ pub struct ThreadEpiphanyContext {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub checkpoint: Option<CoreEpiphanyGraphCheckpoint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub investigation_checkpoint: Option<CoreEpiphanyInvestigationCheckpoint>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub observations: Vec<CoreEpiphanyObservation>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
@ -4480,6 +4510,9 @@ pub struct ThreadEpiphanyUpdatePatch {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub scratch: Option<CoreEpiphanyScratchPad>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub investigation_checkpoint: Option<CoreEpiphanyInvestigationCheckpoint>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub observations: Vec<CoreEpiphanyObservation>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
@ -4532,6 +4565,7 @@ pub enum ThreadEpiphanyStateUpdatedField {
GraphFrontier,
GraphCheckpoint,
Scratch,
InvestigationCheckpoint,
Observations,
Evidence,
Churn,

View file

@ -184,6 +184,7 @@ 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::ThreadEpiphanySceneParams;
use codex_app_server_protocol::ThreadEpiphanySceneRecord;
use codex_app_server_protocol::ThreadEpiphanySceneRecords;
@ -4602,6 +4603,7 @@ impl CodexMessageProcessor {
graphs: patch.graphs.clone(),
graph_frontier: patch.graph_frontier.clone(),
graph_checkpoint: patch.graph_checkpoint.clone(),
investigation_checkpoint: patch.investigation_checkpoint.clone(),
churn: patch.churn.clone(),
observations: patch.observations.clone(),
evidence: patch.evidence.clone(),
@ -4636,6 +4638,7 @@ impl CodexMessageProcessor {
graph_frontier: patch.graph_frontier,
graph_checkpoint: patch.graph_checkpoint,
scratch: patch.scratch,
investigation_checkpoint: patch.investigation_checkpoint,
observations: patch.observations,
evidence,
churn: patch.churn,
@ -4723,6 +4726,7 @@ impl CodexMessageProcessor {
graph_frontier: patch.graph_frontier,
graph_checkpoint: patch.graph_checkpoint,
scratch: patch.scratch,
investigation_checkpoint: patch.investigation_checkpoint,
observations: patch.observations,
evidence: patch.evidence,
churn: patch.churn,
@ -10707,6 +10711,7 @@ fn map_epiphany_scene(state: Option<&EpiphanyThreadState>, loaded: bool) -> Thre
invariant_status_counts: Vec::new(),
graph: ThreadEpiphanySceneGraph::default(),
retrieval: None,
investigation_checkpoint: None,
observations: ThreadEpiphanySceneRecords::default(),
evidence: ThreadEpiphanySceneRecords::default(),
churn: None,
@ -10741,6 +10746,20 @@ fn map_epiphany_scene(state: Option<&EpiphanyThreadState>, loaded: bool) -> Thre
shard_count: retrieval.shards.len() as u32,
dirty_path_count: retrieval.dirty_paths.len() as u32,
}),
investigation_checkpoint: state.investigation_checkpoint.as_ref().map(|checkpoint| {
ThreadEpiphanySceneInvestigationCheckpoint {
checkpoint_id: checkpoint.checkpoint_id.clone(),
kind: checkpoint.kind.clone(),
disposition: checkpoint.disposition,
focus: checkpoint.focus.clone(),
summary: checkpoint.summary.clone(),
next_action: checkpoint.next_action.clone(),
captured_at_turn_id: checkpoint.captured_at_turn_id.clone(),
open_question_count: checkpoint.open_questions.len() as u32,
code_ref_count: checkpoint.code_refs.len() as u32,
evidence_count: checkpoint.evidence_ids.len() as u32,
}
}),
observations: ThreadEpiphanySceneRecords {
total_count: state.observations.len() as u32,
latest: state
@ -11073,6 +11092,7 @@ fn map_epiphany_context(
.then(|| state.graph_frontier.clone())
.flatten(),
checkpoint: state.graph_checkpoint.clone(),
investigation_checkpoint: state.investigation_checkpoint.clone(),
observations,
evidence,
},
@ -11398,6 +11418,7 @@ fn thread_epiphany_patch_has_state_replacements(patch: &ThreadEpiphanyUpdatePatc
|| patch.graph_frontier.is_some()
|| patch.graph_checkpoint.is_some()
|| patch.scratch.is_some()
|| patch.investigation_checkpoint.is_some()
|| patch.churn.is_some()
|| patch.mode.is_some()
}
@ -11496,6 +11517,9 @@ fn epiphany_update_patch_changed_fields(
if patch.scratch.is_some() {
fields.push(ThreadEpiphanyStateUpdatedField::Scratch);
}
if patch.investigation_checkpoint.is_some() {
fields.push(ThreadEpiphanyStateUpdatedField::InvestigationCheckpoint);
}
if !patch.observations.is_empty() {
fields.push(ThreadEpiphanyStateUpdatedField::Observations);
}
@ -12868,6 +12892,29 @@ mod tests {
}],
..Default::default()
}),
investigation_checkpoint: Some(
codex_protocol::protocol::EpiphanyInvestigationCheckpoint {
checkpoint_id: "ix-5".to_string(),
kind: "slice_planning".to_string(),
disposition:
codex_protocol::protocol::EpiphanyInvestigationDisposition::ResumeReady,
focus: "Keep the durable planning packet visible.".to_string(),
summary: Some("Checkpointed the next bounded slice.".to_string()),
next_action: Some("Patch the typed state writer next.".to_string()),
captured_at_turn_id: Some("turn-5".to_string()),
open_questions: vec![
"Should scene and context both surface this?".to_string(),
],
evidence_ids: vec!["ev-scene".to_string()],
code_refs: vec![codex_protocol::protocol::EpiphanyCodeRef {
path: test_path_buf("/repo/app-server/src/codex_message_processor.rs"),
start_line: Some(10692),
end_line: Some(10811),
symbol: Some("map_epiphany_scene".to_string()),
note: None,
}],
},
),
observations: vec![codex_protocol::protocol::EpiphanyObservation {
id: "obs-scene".to_string(),
summary: "Scene is derived".to_string(),
@ -12920,6 +12967,13 @@ mod tests {
scene.retrieval.as_ref().and_then(|r| r.indexed_chunk_count),
Some(8)
);
assert_eq!(
scene
.investigation_checkpoint
.as_ref()
.map(|checkpoint| checkpoint.checkpoint_id.as_str()),
Some("ix-5")
);
assert_eq!(scene.observations.total_count, 1);
assert_eq!(scene.evidence.latest[0].id, "ev-scene");
assert_eq!(
@ -13121,6 +13175,18 @@ mod tests {
summary: Some("Context shard checkpoint".to_string()),
..Default::default()
}),
investigation_checkpoint: Some(
codex_protocol::protocol::EpiphanyInvestigationCheckpoint {
checkpoint_id: "ix-context".to_string(),
kind: "source_gathering".to_string(),
focus: "Trace the exact reflection seam.".to_string(),
next_action: Some(
"Re-gather source before editing if this goes stale.".to_string(),
),
evidence_ids: vec!["ev-linked".to_string()],
..Default::default()
},
),
observations: vec![codex_protocol::protocol::EpiphanyObservation {
id: "obs-1".to_string(),
summary: "Observation carries linked evidence.".to_string(),
@ -13196,6 +13262,13 @@ mod tests {
.map(|checkpoint| checkpoint.checkpoint_id.as_str()),
Some("ck-context")
);
assert_eq!(
context
.investigation_checkpoint
.as_ref()
.map(|checkpoint| checkpoint.checkpoint_id.as_str()),
Some("ix-context")
);
assert_eq!(
context
.observations
@ -13331,6 +13404,7 @@ mod tests {
objective: Some("Keep the event readable".to_string()),
graphs: Some(Default::default()),
graph_frontier: Some(Default::default()),
investigation_checkpoint: Some(Default::default()),
observations: vec![Default::default()],
evidence: vec![Default::default()],
churn: Some(Default::default()),
@ -13343,6 +13417,7 @@ mod tests {
ThreadEpiphanyStateUpdatedField::Objective,
ThreadEpiphanyStateUpdatedField::Graphs,
ThreadEpiphanyStateUpdatedField::GraphFrontier,
ThreadEpiphanyStateUpdatedField::InvestigationCheckpoint,
ThreadEpiphanyStateUpdatedField::Observations,
ThreadEpiphanyStateUpdatedField::Evidence,
ThreadEpiphanyStateUpdatedField::Churn,

View file

@ -31,6 +31,7 @@ use codex_protocol::protocol::EpiphanyGraphCheckpoint;
use codex_protocol::protocol::EpiphanyGraphFrontier;
use codex_protocol::protocol::EpiphanyGraphs;
use codex_protocol::protocol::EpiphanyInvariant;
use codex_protocol::protocol::EpiphanyInvestigationCheckpoint;
use codex_protocol::protocol::EpiphanyModeState;
use codex_protocol::protocol::EpiphanyObservation;
use codex_protocol::protocol::EpiphanyRetrievalState;
@ -110,6 +111,7 @@ pub struct EpiphanyStateUpdate {
pub graph_frontier: Option<EpiphanyGraphFrontier>,
pub graph_checkpoint: Option<EpiphanyGraphCheckpoint>,
pub scratch: Option<EpiphanyScratchPad>,
pub investigation_checkpoint: Option<EpiphanyInvestigationCheckpoint>,
pub observations: Vec<EpiphanyObservation>,
pub evidence: Vec<EpiphanyEvidenceRecord>,
pub churn: Option<EpiphanyChurnState>,
@ -126,6 +128,7 @@ impl EpiphanyStateUpdate {
&& self.graph_frontier.is_none()
&& self.graph_checkpoint.is_none()
&& self.scratch.is_none()
&& self.investigation_checkpoint.is_none()
&& self.observations.is_empty()
&& self.evidence.is_empty()
&& self.churn.is_none()
@ -631,6 +634,17 @@ fn epiphany_state_update_validation_errors(
}
}
if let Some(checkpoint) = update.investigation_checkpoint.as_ref() {
for evidence_id in &checkpoint.evidence_ids {
if !known_evidence_ids.contains(evidence_id.as_str()) {
errors.push(format!(
"investigation checkpoint cites missing evidence id {:?}",
evidence_id
));
}
}
}
errors.extend(epiphany_state_replacement_validation_errors(state, update));
errors
}
@ -643,6 +657,16 @@ fn epiphany_state_replacement_validation_errors(
let validates_graph_target = update.graphs.is_some()
|| update.graph_frontier.is_some()
|| update.graph_checkpoint.is_some();
let mut known_evidence_ids: HashSet<&str> = state
.recent_evidence
.iter()
.filter_map(|evidence| nonempty_id(&evidence.id))
.collect();
for evidence in &update.evidence {
if let Some(id) = nonempty_id(&evidence.id) {
known_evidence_ids.insert(id);
}
}
validate_state_replacement_patch(EpiphanyStateReplacementValidationInput {
active_subgoal_id: update.active_subgoal_id.as_deref(),
@ -662,6 +686,8 @@ fn epiphany_state_replacement_validation_errors(
},
graph_frontier: update.graph_frontier.as_ref(),
graph_checkpoint: update.graph_checkpoint.as_ref(),
investigation_checkpoint: update.investigation_checkpoint.as_ref(),
available_evidence_ids: Some(&known_evidence_ids),
churn: update.churn.as_ref(),
})
}
@ -705,6 +731,9 @@ fn apply_epiphany_state_update(
if let Some(scratch) = update.scratch {
state.scratch = Some(scratch);
}
if let Some(checkpoint) = update.investigation_checkpoint {
state.investigation_checkpoint = Some(checkpoint);
}
if let Some(churn) = update.churn {
state.churn = Some(churn);
}
@ -771,6 +800,15 @@ mod epiphany_update_tests {
&mut state,
EpiphanyStateUpdate {
objective: Some("Keep the map honest".to_string()),
investigation_checkpoint: Some(EpiphanyInvestigationCheckpoint {
checkpoint_id: "ix-1".to_string(),
kind: "slice_planning".to_string(),
focus: "Keep the durable packet small and explicit.".to_string(),
next_action: Some(
"Resume from the packet instead of the ghost transcript.".to_string(),
),
..Default::default()
}),
evidence: vec![EpiphanyEvidenceRecord {
id: "new-evidence".to_string(),
kind: "verification".to_string(),
@ -791,6 +829,13 @@ mod epiphany_update_tests {
assert_eq!(state.revision, 4);
assert_eq!(state.objective.as_deref(), Some("Keep the map honest"));
assert_eq!(state.last_updated_turn_id.as_deref(), Some("turn-1"));
assert_eq!(
state
.investigation_checkpoint
.as_ref()
.map(|checkpoint| checkpoint.checkpoint_id.as_str()),
Some("ix-1")
);
assert_eq!(state.recent_evidence[0].id, "new-evidence");
assert_eq!(state.recent_evidence[1].id, "old-evidence");
assert_eq!(
@ -929,6 +974,30 @@ mod epiphany_update_tests {
.any(|error| error.contains("graph frontier references missing node"))
);
}
#[test]
fn validate_epiphany_state_update_rejects_investigation_checkpoint_with_missing_evidence() {
let update = EpiphanyStateUpdate {
investigation_checkpoint: Some(EpiphanyInvestigationCheckpoint {
checkpoint_id: "ix-missing".to_string(),
kind: "source_gathering".to_string(),
focus: "Trace the compaction seam.".to_string(),
next_action: Some("Re-gather source before implementation.".to_string()),
evidence_ids: vec!["ev-missing".to_string()],
..Default::default()
}),
..Default::default()
};
let errors =
epiphany_state_update_validation_errors(&EpiphanyThreadState::default(), &update);
assert!(
errors
.iter()
.any(|error| error.contains("investigation checkpoint cites missing evidence id"))
);
}
}
fn pending_message_input_item(message: &ResponseItem) -> CodexResult<ResponseInputItem> {

View file

@ -336,6 +336,7 @@ fn sample_epiphany_state_for_prompt() -> EpiphanyThreadState {
open_question_ids: vec!["q-boundary".to_string()],
open_gap_ids: vec!["gap-resume-proof".to_string()],
}),
investigation_checkpoint: None,
scratch: Some(EpiphanyScratchPad {
summary: Some("Inject the state as a developer fragment, not as a user message.".to_string()),
hypothesis: Some("Keeping the block bounded will help resume without making prompt sludge.".to_string()),

View file

@ -3017,6 +3017,9 @@ pub struct EpiphanyThreadState {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "EpiphanyScratchPad | null")]
pub scratch: Option<EpiphanyScratchPad>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "EpiphanyInvestigationCheckpoint | null")]
pub investigation_checkpoint: Option<EpiphanyInvestigationCheckpoint>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyObservation>")]
pub observations: Vec<EpiphanyObservation>,
@ -3258,6 +3261,41 @@ pub struct EpiphanyScratchPad {
pub notes: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum EpiphanyInvestigationDisposition {
#[default]
ResumeReady,
RegatherRequired,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyInvestigationCheckpoint {
pub checkpoint_id: String,
pub kind: String,
pub disposition: EpiphanyInvestigationDisposition,
pub focus: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub summary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub next_action: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub captured_at_turn_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub open_questions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyCodeRef>")]
pub code_refs: Vec<EpiphanyCodeRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub evidence_ids: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyObservation {
pub id: String,