Add Epiphany graph query surface

This commit is contained in:
Meta Vens 2026-04-29 21:49:47 +01:00
commit 5c62a65052
22 changed files with 1608 additions and 19 deletions

View file

@ -56,6 +56,7 @@ The rule is:
| `thread/epiphany/jobs` | read-only reflection | landed, live-smoked | Derived indexing, remap, verification, and specialist-progress slots from typed state and retrieval summaries, with durable launcher metadata plus live backend overlay when a real owner exists. |
| `thread/epiphany/freshness` | read-only reflection | landed, live-smoked | Retrieval and graph freshness lens derived from retrieval summaries plus graph frontier/churn state, with watcher-backed invalidation inputs for loaded threads. |
| `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/graphQuery` | read-only reflection | landed, live-smoked | Bounded graph traversal over authoritative typed state: explicit nodes/edges, path/symbol matches, edge-kind matches, neighbor walks, and frontier neighborhoods with architecture/dataflow links. |
| `thread/epiphany/pressure` | read-only reflection | landed, live-smoked | Context-pressure gauge derived from token telemetry and recorded auto-compact/context limits. |
| `thread/epiphany/reorient` | read-only policy reflection | landed, live-smoked | Bounded CRRC reorientation verdict derived from checkpoint, freshness, watcher, and pressure signals; returns resume vs regather without acting on the decision. |
| `thread/epiphany/crrc` | read-only coordinator reflection | landed, live-smoked | Composes pressure, reorientation verdict, bound worker status/result, and available actions into one recommendation without launching, accepting, compacting, or mutating state. |
@ -84,6 +85,7 @@ The following must stay read-only:
- `thread/epiphany/jobs`
- `thread/epiphany/freshness`
- `thread/epiphany/context`
- `thread/epiphany/graphQuery`
- `thread/epiphany/pressure`
- `thread/epiphany/reorient`
- `thread/epiphany/crrc`
@ -94,7 +96,7 @@ license to mutate map/evidence/churn state as a side effect.
## Reflection Authority
Scene, jobs, freshness, context, reorient, CRRC, and GUI surfaces may compress state for humans and
Scene, jobs, freshness, context, graphQuery, reorient, CRRC, and GUI surfaces may compress state for humans and
clients. They may not invent canonical understanding.
Reflection surfaces should:
@ -144,7 +146,7 @@ Metaphor is compression after source context. It is not decoration for guesses.
These are not landed yet:
- richer evidence-range and graph-shard inspection beyond the landed context shard
- richer evidence UI and graph steering beyond the landed context and graph traversal reads
- automatic watcher-driven graph/retrieval/invariant invalidation policy on top of the landed freshness reflection
- automatic tool-output observation promotion
- typed turn intent before broad mutation
@ -176,7 +178,7 @@ Out of scope for the MVP:
- arbitrary specialist marketplaces
- broad automatic background scheduling
- GUI-first workflows
- GUI-as-source-of-truth workflows
- automatic promotion of all tool output
- a second job backend unless the current `agent_jobs` adapter blocks read-back
or interruption
@ -317,6 +319,25 @@ It does not run retrieval, draft proposals, promote observations, mutate
`SessionState`, append rollout items, or emit `thread/epiphany/stateUpdated`.
It is a bounded lens, not a clerk with a pen.
## Graph Query Surface Direction
The first graph traversal read is landed as `thread/epiphany/graphQuery`.
It selects and walks existing typed graph state only:
- explicit architecture/dataflow node ids and edge ids
- path and symbol matches from code refs on nodes, edges, and links
- bounded incoming, outgoing, or bidirectional neighbor walks
- active frontier neighborhoods
- edge-kind matches
- architecture/dataflow links by default
- current graph frontier and graph checkpoint identity
- matched selectors and missing explicit ids
It does not run retrieval, draft proposals, promote observations, index code,
mutate `SessionState`, append rollout items, schedule workers, or emit
`thread/epiphany/stateUpdated`. It is a traversal lens, not a map editor.
## Reorientation Policy
The first bounded CRRC policy surface is landed as `thread/epiphany/reorient`.

View file

@ -10,7 +10,7 @@ The important question for this file is not "what is Epiphany supposed to become
Current Epiphany is a forked Codex harness with a typed modeling spine.
The spine exists in twenty-four live paths:
The spine exists in 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).
@ -28,6 +28,7 @@ The spine exists in twenty-four live paths:
- explicit job control: app-server now declares experimental `thread/epiphany/jobLaunch` and `thread/epiphany/jobInterrupt`, routes them through core launch/interrupt handlers that can open the live or direct sqlite state runtime, create or cancel the current `agent_jobs` backend binding, persist durable `job_bindings` revisions through the authoritative Epiphany state path, interrupt live worker threads when needed, and emit `thread/epiphany/stateUpdated` with source `jobLaunch` or `jobInterrupt` instead of pretending jobs reflection is the scheduler in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:389), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4850), [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:418), [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:493), [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:657), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4992).
- freshness reflection: app-server declares experimental read-only `thread/epiphany/freshness`, reads the same live/stored thread view used by scene/jobs/context, fills live retrieval state for loaded threads, and derives one bounded retrieval/graph freshness lens plus, for loaded threads, watcher-backed invalidation inputs with watched root, changed paths, mapped graph-node hits, active-frontier hits, and revision/source identity without mutating, scheduling, notifying, or performing automatic semantic invalidation in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:350), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4167), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4259), [epiphany_invalidation.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/epiphany_invalidation.rs:19), [epiphany_invalidation.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/epiphany_invalidation.rs:67), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4219), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11073).
- 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:355), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4253), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4261), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11152).
- graph traversal reflection: app-server declares experimental read-only `thread/epiphany/graphQuery`, reads the same live Epiphany state view used by context, and derives explicit node/edge lookup, path/symbol matches, bounded neighbors, edge-kind matches, and frontier neighborhoods with architecture/dataflow links plus frontier/checkpoint identity, matched selectors, and missing ids without retrieval, proposal, promotion, indexing, notification, mutation, or scheduling in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:379), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4720), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:5060), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:15201).
- 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:360), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4361), [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:4306), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11028).
- pre-compaction checkpoint intervention: app-server `TokenCount` handling now checks the same pressure mapper, and for loaded Epiphany threads crossing `shouldPrepareCompaction` it steers the active turn once with a bounded CRRC checkpoint directive to bank working context before compaction/reorientation. The per-turn dedupe bit lives in listener `ThreadState`, and the intervention uses existing turn steering rather than a new scheduler in [bespoke_event_handling.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/bespoke_event_handling.rs:1467), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:15405), and [thread_state.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/thread_state.rs:144).
- reorientation policy: app-server declares experimental read-only `thread/epiphany/reorient`, reuses the same live/stored thread view plus mapped pressure/freshness/watcher signals, and derives a bounded `resume`/`regather` verdict with checkpoint-path/frontier reasons without mutating, compacting, scheduling, notifying, or continuing work in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:365), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4514), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4394), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11424).
@ -82,6 +83,8 @@ flowchart TD
QF --> Q
N --> QC["thread/epiphany/context derives targeted state shard"]
QC --> Q
N --> QG["thread/epiphany/graphQuery traverses graph neighborhoods"]
QG --> Q
A --> QP["TokenUsageInfo stores usage, context window, and auto-compact limit"]
QP --> QQ["thread/epiphany/pressure derives read-only context pressure"]
QQ --> Q
@ -150,6 +153,7 @@ What this means in plain English:
- The app-server can expose read-only job/progress slots derived from typed state and retrieval summaries without creating a scheduler.
- The app-server can expose a read-only freshness lens derived from retrieval summaries plus graph frontier/churn state and, for loaded threads, watcher-backed invalidation telemetry without turning file staleness into a hidden worker.
- The app-server can expose a targeted read-only context shard for clients that need graph/evidence detail without pulling the full typed state.
- The app-server can expose read-only graph traversal for clients and agents that need frontier neighborhoods, path/symbol matches, or local blast-radius walks without turning traversal into proposal or promotion.
- The app-server can expose context pressure derived from real token telemetry without starting compaction or pretending vibes are telemetry.
- A loaded thread can ask for a typed observation/evidence proposal without mutating state.
- A loaded thread can ask a verifier-backed promotion gate to reject or apply that proposal.
@ -1195,6 +1199,77 @@ The response is a bounded state shard for a client or model-facing coordinator t
Context projection is read-only. It does not run retrieval, draft proposals, promote observations, mutate `SessionState`, append rollout items, or emit notifications. It opens the binder to the requested page; it does not pick up the pen.
## Flow 15b: Graph Query Traversal Control Flow
### Input
A client sends experimental `thread/epiphany/graphQuery` with:
- `threadId`
- `query.kind`: `node`, `path`, `frontierNeighborhood`, or `neighbors`
- optional explicit `nodeIds` and `edgeIds`
- optional `paths`, `symbols`, and `edgeKinds`
- optional `direction`, defaulting to `both`
- optional `depth`, defaulting to `1` and capped at `3`
- optional `includeLinks`, defaulting true
### Plain-language role
Graph query projection is the traversal lens. Context opens a requested binder page; graphQuery walks the local strings tied to that page. Implementation and verifier agents can ask for the current frontier neighborhood, a path/symbol neighborhood, or a bounded neighbor walk without pulling the whole state or asking retrieval to rediscover what the typed graph already knows.
Response shape:
- `threadId`
- `source`
- `stateStatus`
- optional `stateRevision`
- `graph`
- `architectureNodes`
- `architectureEdges`
- `dataflowNodes`
- `dataflowEdges`
- `links`
- optional `frontier`
- optional `checkpoint`
- `matched`
- `nodeIds`
- `edgeIds`
- `paths`
- `symbols`
- `edgeKinds`
- `missing`
- `nodeIds`
- `edgeIds`
### Mechanism
The app-server handler:
1. parses the thread id.
2. requires a loaded thread.
3. calls `read_thread_view(..., include_turns: false)` to reuse the live Epiphany projection path.
4. passes the optional `thread.epiphany_state` and query into `map_epiphany_graph_query`.
5. returns the selected graph records, current frontier/checkpoint, matched selectors, and missing explicit ids.
`map_epiphany_graph_query`:
1. returns missing state with an empty graph and echoes requested node/edge ids when no Epiphany state exists.
2. starts from explicit node/edge ids.
3. for `path`, matches node, edge, and link code refs by requested path and optional symbol.
4. for `frontierNeighborhood`, starts from active frontier node/edge ids and expands neighbors.
5. for `neighbors`, expands neighbors from explicit node ids.
6. adds edge-kind matches when requested.
7. follows architecture/dataflow links by default.
8. returns selected graph nodes, selected and incident graph edges, graph links, frontier, checkpoint, matched selectors, and unresolved explicit ids.
### Output
The response is a bounded traversal result for agents that need to move around the typed graph. The Phase 6 graph query smoke proved missing-state graphQuery reports live source without inventing graph records, frontier traversal returns one-hop architecture neighbors plus linked dataflow state, path/symbol lookup reports the matched code ref, no `thread/epiphany/stateUpdated` notification is emitted, and final thread state revision does not change.
### Invariant
Graph query projection is read-only. It does not run retrieval, draft proposals, index code, promote observations, mutate `SessionState`, append rollout items, schedule work, or emit notifications. It is a map traversal, not a cartographer with a pen.
## Flow 16: Context Pressure Reflection Control Flow
### Input
@ -1349,7 +1424,7 @@ These are deliberately not shipped yet:
- no code-intelligence graph.
- no broad specialist-agent scheduler or marketplace; only fixed manual modeling/checkpoint and verification/review role templates exist.
- no Epiphany-owned long-running job executor beyond the current runtime `agent_jobs` seam.
- no GUI implementation beyond the read-only scene, jobs, freshness, context, and pressure projection surfaces.
- no GUI source of truth; the current Tauri/React shell reflects and steers existing app-server surfaces.
- no durable retrieval-summary write from `thread/epiphany/retrieve`.
The current system is therefore not "the model maintains a map automatically." More precisely:
@ -1363,6 +1438,7 @@ stored Epiphany state
-> reflected as read-only job/progress slots for clients
-> reflected as a read-only freshness lens for clients
-> reflected as targeted read-only graph/evidence context shards for clients
-> reflected as bounded read-only graph traversal for clients and agents
-> reflected as read-only context pressure from token telemetry
-> reflected as a read-only resume/regather verdict from checkpoint + freshness + watcher + pressure
-> reflected as a read-only CRRC recommendation over verdict + worker/result state
@ -1378,7 +1454,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 live bound-job notification, not the first freshness lens, not the first watcher-backed invalidation read, not the first targeted graph/evidence context read, not the first context-pressure gauge, not the first durable investigation checkpoint packet, not the first bounded reorientation verdict, not the first thin launcher seam, not the first explicit launch/interrupt authority surface over that seam, not the first runtime consumer that can act on the reorientation verdict without becoming a hidden tyrant, not the first read-back surface for that worker's findings, not the first explicit acceptance write for completed findings, not the first read-only CRRC coordinator recommendation, not the first role ownership board, not the first fixed role specialist launch/read-back templates, and no longer the first dogfood-facing operator view over the landed loop. 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. Runtime `agent_job_progress` background events now also emit `thread/epiphany/jobsUpdated` with changed bound-job payloads so clients no longer need to poll `thread/epiphany/jobs` just to watch one real owner move, the bound jobs now reflect launcher id, authority scope, and backend kind/job id instead of treating raw runtime pointers as the architecture, `jobLaunch` / `jobInterrupt` can create or tear down those bindings without pretending reflection is authority, `reorientLaunch` can launch one fixed `reorient-worker` job from the checkpoint verdict without turning the verdict itself into a writer, `reorientResult` can read the worker's structured output back as a reviewable finding without promoting it, `reorientAccept` can explicitly bank a completed finding into typed observation/evidence plus optional scratch/checkpoint state, `roleLaunch` can launch fixed modeling/checkpoint and verification/review specialists over that same bounded seam, `roleResult` can read their structured outputs back without mutation, and `crrc` can recommend the next explicit action over that loop without doing the action itself. The scene, jobs, roles, role-result, freshness, context, pressure, reorient, crrc, reorient-result, and MVP status surfaces give clients compact or bounded views without making the GUI/client authoritative. The remaining missing organs are a second backend if `agent_jobs` stops being enough, automatic scheduling if dogfood proves it is needed, and richer product ergonomics after the MVP loop has been tested by a human.
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 live bound-job notification, not the first freshness lens, not the first watcher-backed invalidation read, not the first targeted graph/evidence context read, not the first bounded graph traversal read, not the first context-pressure gauge, not the first durable investigation checkpoint packet, not the first bounded reorientation verdict, not the first thin launcher seam, not the first explicit launch/interrupt authority surface over that seam, not the first runtime consumer that can act on the reorientation verdict without becoming a hidden tyrant, not the first read-back surface for that worker's findings, not the first explicit acceptance write for completed findings, not the first read-only CRRC coordinator recommendation, not the first role ownership board, not the first fixed role specialist launch/read-back templates, and no longer the first dogfood-facing operator view over the landed loop. 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. Runtime `agent_job_progress` background events now also emit `thread/epiphany/jobsUpdated` with changed bound-job payloads so clients no longer need to poll `thread/epiphany/jobs` just to watch one real owner move, the bound jobs now reflect launcher id, authority scope, and backend kind/job id instead of treating raw runtime pointers as the architecture, `jobLaunch` / `jobInterrupt` can create or tear down those bindings without pretending reflection is authority, `reorientLaunch` can launch one fixed `reorient-worker` job from the checkpoint verdict without turning the verdict itself into a writer, `reorientResult` can read the worker's structured output back as a reviewable finding without promoting it, `reorientAccept` can explicitly bank a completed finding into typed observation/evidence plus optional scratch/checkpoint state, `roleLaunch` can launch fixed modeling/checkpoint and verification/review specialists over that same bounded seam, `roleResult` can read their structured outputs back without mutation, and `crrc` can recommend the next explicit action over that loop without doing the action itself. The scene, jobs, roles, role-result, freshness, context, graphQuery, pressure, reorient, crrc, reorient-result, and MVP status surfaces give clients compact or bounded views without making the GUI/client authoritative. The remaining missing organs are a second backend if `agent_jobs` stops being enough, automatic scheduling if dogfood proves it is needed, and richer product ergonomics after the MVP loop has been tested by a human.
```text
model/tool observations
@ -1392,9 +1468,9 @@ model/tool observations
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, reflect a thin launcher seam over bound work, explicitly launch or interrupt that bound work through a bounded authority surface, emit live bound-job progress notifications from runtime `agent_job_progress` events, project a read-only freshness lens over retrieval and graph staleness, layer watcher-backed invalidation telemetry onto that lens for loaded threads, 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, turn those signals into a read-only resume-versus-regather verdict, recommend the next explicit CRRC action over the verdict and worker/result state, print that recommendation in a dogfood operator view, explicitly launch one bounded `reorient-worker` job from that verdict, read its result back for review, explicitly accept a completed finding into typed state, 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 automatically, execute Epiphany-owned long-running jobs beyond the borrowed runtime `agent_jobs` backend, or coordinate broad specialist ownership. Teeth are installed; chewing is supervised; the first bell rings; the first wall board exists; the first shift board exists; the first freshness clipboard exists; the first door sensor now exists too; the first binder lens exists; the first pressure gauge exists; the first ember packet exists; the first verdict slip now exists too; the first bounded launch lever and stop switch now exist too; the first explicit wakeup-hand now exists too; the first coordinator clipboard now exists too; the first operator readout exists too. The next slices are about giving real roles real ownership, 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, reflect a thin launcher seam over bound work, explicitly launch or interrupt that bound work through a bounded authority surface, emit live bound-job progress notifications from runtime `agent_job_progress` events, project a read-only freshness lens over retrieval and graph staleness, layer watcher-backed invalidation telemetry onto that lens for loaded threads, project a targeted graph/evidence context shard from the map, traverse bounded graph neighborhoods and path/symbol matches 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, turn those signals into a read-only resume-versus-regather verdict, recommend the next explicit CRRC action over the verdict and worker/result state, print that recommendation in a dogfood operator view, explicitly launch one bounded `reorient-worker` job from that verdict, read its result back for review, explicitly accept a completed finding into typed state, 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 automatically, execute Epiphany-owned long-running jobs beyond the borrowed runtime `agent_jobs` backend, or coordinate broad specialist ownership. Teeth are installed; chewing is supervised; the first bell rings; the first wall board exists; the first shift board exists; the first freshness clipboard exists; the first door sensor now exists too; the first binder lens exists; the first graph-walk lens exists; the first pressure gauge exists; the first ember packet exists; the first verdict slip now exists too; the first bounded launch lever and stop switch now exist too; the first explicit wakeup-hand now exists too; the first coordinator clipboard now exists too; the first operator readout exists too. The next slices are about giving real roles real ownership, not jawbones.
The role board now exists too. Current Epiphany can project implementation, modeling/checkpoint, verification/review, and reorientation ownership through `thread/epiphany/roles`; what remains is giving modeling and verification explicit launch/read-back templates without growing an ambient dispatcher.
The role board and fixed specialist templates now exist too. Current Epiphany can project implementation, modeling/checkpoint, verification/review, and reorientation ownership through `thread/epiphany/roles`, explicitly launch fixed modeling/verification workers, read their findings back, and keep semantic acceptance review-gated. What remains is dogfooding the operator loop until real product friction, not imagined architecture guilt, tells us the next organ.
## Verification Hooks
@ -1418,6 +1494,7 @@ Current tests cover the landed flows at useful seams:
- live app-server smoke for the first Phase 6 freshness boundary in `tools/epiphany_phase6_freshness_smoke.py`.
- live app-server smoke for watcher-backed invalidation inputs inside the Phase 6 freshness boundary in `tools/epiphany_phase6_invalidation_smoke.py`.
- live app-server smoke for the first Phase 6 context boundary in `tools/epiphany_phase6_context_smoke.py`.
- live app-server smoke for the first Phase 6 graph traversal boundary in `tools/epiphany_phase6_graph_query_smoke.py`.
- live app-server smoke for the first Phase 6 pressure boundary in `tools/epiphany_phase6_pressure_smoke.py`.
- live app-server smoke for the first Phase 6 reorientation policy boundary in `tools/epiphany_phase6_reorient_smoke.py`.
- live app-server smoke for the first bounded reorient-launch runtime consumer plus CRRC coordinator recommendations in `tools/epiphany_phase6_reorient_launch_smoke.py`.
@ -1440,6 +1517,7 @@ $env:CARGO_TARGET_DIR='C:\Users\Meta\.cargo-target-codex'; cargo test -p codex-a
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_freshness_smoke.py'
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_invalidation_smoke.py'
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_context_smoke.py'
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_graph_query_smoke.py'
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_pressure_smoke.py'
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_reorient_smoke.py'
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_reorient_launch_smoke.py'

View file

@ -67,6 +67,8 @@ The landed machine now has:
- live Phase 6 watcher-backed invalidation smoke coverage in `tools/epiphany_phase6_invalidation_smoke.py`
- read-only Phase 6 targeted state-shard reflection through `thread/epiphany/context`
- live Phase 6 context app-server smoke coverage in `tools/epiphany_phase6_context_smoke.py`
- read-only Phase 6 graph traversal through `thread/epiphany/graphQuery`
- live Phase 6 graph traversal smoke coverage in `tools/epiphany_phase6_graph_query_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
@ -109,6 +111,7 @@ These boundaries are more important than the individual method names:
- `thread/epiphany/reorientAccept` is an explicit acceptance write for completed reorient-worker findings, not automatic promotion, scheduling, or permission to continue without review.
- `thread/epiphany/freshness` is a derived reflection, not automatic watcher-driven invalidation, a mutation gate, or a hidden refresh scheduler.
- `thread/epiphany/context` is a targeted reflection, not a state writer or hidden proposal engine.
- `thread/epiphany/graphQuery` is bounded graph traversal over authoritative typed state, not retrieval, proposal, promotion, scheduling, indexing, or a state writer.
- `thread/epiphany/pressure` is a context-pressure reflection, not an automatic compactor, scheduler, or CRRC coordinator.
- `thread/epiphany/reorient` is a bounded policy verdict, not an automatic runtime coordinator, scheduler, compactor, or hidden state writer.
- `thread/epiphany/crrc` is a read-only coordinator recommendation over existing signals, not a scheduler, launch button, acceptance gate, compactor, or hidden state writer.
@ -311,7 +314,7 @@ These remain later work:
- watcher-driven semantic invalidation
- automatic observation promotion from tool output
- richer evidence and graph-shard inspection beyond the landed targeted context read
- richer evidence UI and graph steering beyond the landed targeted context and graph traversal reads
- richer role ergonomics after the fixed single-user coordinator proves useful
- mutation gates that warn or block broad writes when map freshness is stale
- broader CRRC runtime coordination beyond the landed narrow safe-boundary compact, fixed reorient-worker launch, and pre-compaction checkpoint steering actions
@ -360,6 +363,12 @@ Before modifying targeted context-shard behavior, run:
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_context_smoke.py'
```
Before modifying graph traversal behavior, run:
```powershell
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_graph_query_smoke.py'
```
For app-server protocol changes, expect to run the relevant protocol tests,
regenerate stable schema fixtures when needed, and verify the generated tree is
intentional.

View file

@ -28,7 +28,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/roles`, `thread/epiphany/freshness`, `thread/epiphany/context`, `thread/epiphany/pressure`, `thread/epiphany/reorient`, `thread/epiphany/crrc`, `thread/epiphany/coordinator`, `thread/epiphany/reorientResult`, and `thread/epiphany/roleResult`; durable `jobBindings` now act as a thin Epiphany-owned launcher seam with launcher id, authority scope, and backend kind/job id, `thread/epiphany/jobLaunch` / `thread/epiphany/jobInterrupt` / `thread/epiphany/roleLaunch` now provide explicit bounded authority over that seam, jobs can overlay it onto live runtime `agent_jobs` progress for loaded threads, and `thread/epiphany/jobsUpdated` now translates live `agent_job_progress:{json}` background events into changed launcher-bound notifications without polling or scheduling. Freshness carries watcher-backed invalidation inputs, roles project implementation/modeling/verification/reorientation ownership from existing signals without becoming a scheduler, `roleLaunch` can launch only fixed modeling/checkpoint or verification/review specialists, `roleResult` reads those outputs back as reviewable findings without mutation, reorient turns checkpoint plus freshness/pressure/watcher signals into a read-only resume-versus-regather verdict, `thread/epiphany/reorientLaunch` is the explicit runtime consumer over that verdict, `thread/epiphany/reorientResult` reads the launched worker output back as a reviewable finding without mutation or promotion, `thread/epiphany/reorientAccept` explicitly banks completed findings into accepted observation/evidence plus optional scratch/checkpoint state, `thread/epiphany/crrc` recommends the next explicit CRRC action without launching, accepting, compacting, scheduling, or mutating, and `thread/epiphany/coordinator` composes those signals into a fixed-lane MVP action recommendation without becoming a writer.
- Phase 6 has read-only `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/roles`, `thread/epiphany/freshness`, `thread/epiphany/context`, `thread/epiphany/graphQuery`, `thread/epiphany/pressure`, `thread/epiphany/reorient`, `thread/epiphany/crrc`, `thread/epiphany/coordinator`, `thread/epiphany/reorientResult`, and `thread/epiphany/roleResult`; durable `jobBindings` now act as a thin Epiphany-owned launcher seam with launcher id, authority scope, and backend kind/job id, `thread/epiphany/jobLaunch` / `thread/epiphany/jobInterrupt` / `thread/epiphany/roleLaunch` now provide explicit bounded authority over that seam, jobs can overlay it onto live runtime `agent_jobs` progress for loaded threads, and `thread/epiphany/jobsUpdated` now translates live `agent_job_progress:{json}` background events into changed launcher-bound notifications without polling or scheduling. Freshness carries watcher-backed invalidation inputs, graphQuery traverses authoritative typed graph neighborhoods and path/symbol matches without mutation, roles project implementation/modeling/verification/reorientation ownership from existing signals without becoming a scheduler, `roleLaunch` can launch only fixed modeling/checkpoint or verification/review specialists, `roleResult` reads those outputs back as reviewable findings without mutation, reorient turns checkpoint plus freshness/pressure/watcher signals into a read-only resume-versus-regather verdict, `thread/epiphany/reorientLaunch` is the explicit runtime consumer over that verdict, `thread/epiphany/reorientResult` reads the launched worker output back as a reviewable finding without mutation or promotion, `thread/epiphany/reorientAccept` explicitly banks completed findings into accepted observation/evidence plus optional scratch/checkpoint state, `thread/epiphany/crrc` recommends the next explicit CRRC action without launching, accepting, compacting, scheduling, or mutating, and `thread/epiphany/coordinator` composes those signals into a fixed-lane MVP action recommendation without becoming a writer.
- `tools/epiphany_mvp_status.py` is the first dogfood operator view. It starts or reads a thread through app-server and prints scene, pressure, reorient, jobs, roles, modeling/verification role result read-backs, reorient result, and CRRC recommendation as text or JSON.
- `tools/epiphany_mvp_coordinator.py` is the first auditable fixed-lane coordinator runner. It starts or reads a thread through app-server, follows the harness-native coordinator action, can auto-launch modeling, verification, or reorient-worker jobs, keeps semantic findings review-gated by default, and writes summary, steps, rendered snapshots, transcript, stderr, and final next-action artifacts under `.epiphany-dogfood/coordinator` or a caller-provided artifact directory.
- Native CRRC automation is now landed only at turn-complete safe boundaries. It may submit `Op::Compact` for coordinator-approved `compactRehydrateReorient`, and it may launch the fixed `reorient-worker` for coordinator-approved `launchReorientWorker`. It does not auto-launch modeling/verification, accept findings, promote evidence, edit implementation code, or keep going after reviewable semantic output.
@ -85,6 +85,7 @@ The current spine:
- read-only role ownership reflection through `thread/epiphany/roles`
- read-only retrieval/graph freshness reflection plus watcher-backed invalidation inputs through `thread/epiphany/freshness`
- read-only targeted state-shard reflection through `thread/epiphany/context`
- read-only graph traversal through `thread/epiphany/graphQuery`
- read-only context-pressure reflection through `thread/epiphany/pressure`
- read-only CRRC reorientation policy through `thread/epiphany/reorient`
- read-only CRRC next-action recommendation through `thread/epiphany/crrc`
@ -97,6 +98,7 @@ The current spine:
- live freshness app-server smoke through `tools/epiphany_phase6_freshness_smoke.py`
- live watcher-backed invalidation smoke through `tools/epiphany_phase6_invalidation_smoke.py`
- live context app-server smoke through `tools/epiphany_phase6_context_smoke.py`
- live graph traversal app-server smoke through `tools/epiphany_phase6_graph_query_smoke.py`
- live pressure app-server smoke through `tools/epiphany_phase6_pressure_smoke.py`
- live reorientation app-server smoke through `tools/epiphany_phase6_reorient_smoke.py`
- live reorient-guided worker launch smoke through `tools/epiphany_phase6_reorient_launch_smoke.py`
@ -118,6 +120,7 @@ The exact current control flow is documented in
- `thread/epiphany/jobs` is read-only.
- `thread/epiphany/freshness` is read-only.
- `thread/epiphany/context` is read-only.
- `thread/epiphany/graphQuery` is read-only.
- `thread/epiphany/pressure` is read-only.
- `thread/epiphany/reorient` is read-only.
- `thread/epiphany/crrc` is read-only.
@ -174,6 +177,12 @@ For targeted context-shard behavior changes, run:
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_context_smoke.py'
```
For graph traversal behavior changes, run:
```powershell
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_graph_query_smoke.py'
```
For context-pressure reflection behavior changes, run:
```powershell
@ -271,6 +280,12 @@ 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.
The Phase 6 graph traversal slice is also landed. It exposes read-only
`thread/epiphany/graphQuery` for explicit node/edge lookup, path/symbol lookup,
bounded neighbor traversal, and frontier-neighborhood inspection over
authoritative typed state. It returns graph records, frontier/checkpoint
identity, matched selectors, and missing ids without mutating or notifying.
The bounded CRRC coordination/policy layer is now also landed as read-only
`thread/epiphany/reorient`. It consumes the landed pressure, freshness,
watcher, and investigation-checkpoint signals to decide whether a checkpoint
@ -318,10 +333,11 @@ GUI-as-source-of-truth.
Live `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/roles`,
`thread/epiphany/freshness`, `thread/epiphany/context`,
`thread/epiphany/pressure`, `thread/epiphany/reorient`,
`thread/epiphany/graphQuery`, `thread/epiphany/pressure`, `thread/epiphany/reorient`,
`thread/epiphany/crrc`, `thread/epiphany/coordinator`,
`tools/epiphany_mvp_status.py`, and `tools/epiphany_mvp_coordinator.py` smokes
are now guardrails, not the next organs.
`tools/epiphany_mvp_status.py`, `tools/epiphany_mvp_coordinator.py`, and
`tools/epiphany_phase6_graph_query_smoke.py` smokes are now guardrails, not the
next organs.
## Not Yet

View file

@ -29,3 +29,4 @@
{"ts":"2026-04-29T14:35:07+00:00","type":"implementation","status":"ok","note":"Self-dogfood exposed an artifact-honesty blocker in tools/epiphany_mvp_dogfood.py: the manifest listed vanilla comparison artifacts that were not written. The runner now writes truthful vanilla-reference and comparison artifacts for skipped, failed, or completed reference runs."}
{"ts":"2026-04-29T14:55:37+00:00","type":"design","status":"ok","note":"Chose the MVP GUI path: a local Tauri v2 + React operator console over existing app-server status/coordinator/role/reorient/job/artifact surfaces. The GUI reflects and steers typed Epiphany state but does not become a source of truth."}
{"ts":"2026-04-29T16:22:19+00:00","type":"verification","status":"ok","note":"GUI operator MVP now prepares durable checkpoint state, shows status/coordinator/job/role/artifact surfaces, and exposes bounded status/coordinator artifacts plus fixed modeling, verification, and reorient launch/read controls with review-gated reorient acceptance. Verified with py_compile, npm build, browser visual smoke, cargo fmt/check, Tauri debug build, and a live prepareCheckpoint -> readModelingResult bridge probe."}
{"ts": "2026-04-29T20:49:06+00:00", "type": "verification", "status": "ok", "note": "thread/epiphany/graphQuery landed as a read-only graph traversal surface: explicit node/edge lookup, path/symbol matching, edge-kind matching, bounded neighbor walks, and frontier neighborhoods over authoritative typed state, live-smoked without stateUpdated notifications or revision mutation."}

View file

@ -54,7 +54,7 @@ data_flow:
active_subgoals:
- Treat Phase 5 update/distill/propose/promote/read/notification as complete enough unless a concrete regression appears.
- Preserve read-only boundaries for retrieve, distill, propose, scene, jobs, roles, roleResult, freshness, context, pressure, reorient, crrc, coordinator, and reorientResult.
- Preserve read-only boundaries for retrieve, distill, propose, scene, jobs, roles, roleResult, freshness, context, graphQuery, pressure, reorient, crrc, coordinator, and reorientResult.
- Dogfood the landed Phase 6 coordinator, limited safe-boundary CRRC automation, and pre-compaction checkpoint intervention before adding larger organs.
- Preserve review gates for semantic findings; automatic CRRC may compact or launch the fixed reorient-worker, but not accept or promote.
- Keep persistent project state ruthlessly distilled so rehydration strengthens the next turn instead of bloating it.
@ -63,9 +63,9 @@ accepted_design:
- Epiphany is a fork-level modeling architecture, not merely a Codex collaboration mode.
- Authoritative Epiphany state lives in Codex protocol/core session and rollout state.
- The prompt reads typed state through a bounded `<epiphany_state>` developer fragment.
- Clients read typed state through `Thread.epiphanyState`, compact reflections such as `thread/epiphany/scene`, `thread/epiphany/jobs`, and `thread/epiphany/freshness`, targeted state shards through `thread/epiphany/context`, runtime pressure through `thread/epiphany/pressure`, bounded CRRC reorientation verdicts through `thread/epiphany/reorient`, read-only CRRC next-action recommendations through `thread/epiphany/crrc`, role ownership through `thread/epiphany/roles`, reviewable role output through `thread/epiphany/roleResult`, and live bound-job progress through `thread/epiphany/jobsUpdated`.
- Clients read typed state through `Thread.epiphanyState`, compact reflections such as `thread/epiphany/scene`, `thread/epiphany/jobs`, and `thread/epiphany/freshness`, targeted state shards through `thread/epiphany/context`, bounded graph traversal through `thread/epiphany/graphQuery`, runtime pressure through `thread/epiphany/pressure`, bounded CRRC reorientation verdicts through `thread/epiphany/reorient`, read-only CRRC next-action recommendations through `thread/epiphany/crrc`, role ownership through `thread/epiphany/roles`, reviewable role output through `thread/epiphany/roleResult`, and live bound-job progress through `thread/epiphany/jobsUpdated`.
- Durable typed state writes route through `thread/epiphany/update`, accepted `thread/epiphany/promote`, or the bounded `thread/epiphany/jobLaunch`, `thread/epiphany/jobInterrupt`, `thread/epiphany/reorientLaunch`, and `thread/epiphany/roleLaunch` authority surfaces when they mutate `job_bindings`.
- `thread/epiphany/retrieve`, `thread/epiphany/distill`, `thread/epiphany/propose`, `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/freshness`, `thread/epiphany/context`, `thread/epiphany/pressure`, and `thread/epiphany/reorient` stay read-only.
- `thread/epiphany/retrieve`, `thread/epiphany/distill`, `thread/epiphany/propose`, `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/freshness`, `thread/epiphany/context`, `thread/epiphany/graphQuery`, `thread/epiphany/pressure`, and `thread/epiphany/reorient` stay read-only.
- `thread/epiphany/index` writes the retrieval catalog, not durable Epiphany understanding.
- Qdrant is the preferred persistent semantic backend; BM25 remains the bootstrap/fallback/control path.
- `epiphany-core` owns prompt rendering, rollout replay helpers, retrieval/indexing, distillation, proposal, and promotion policy where possible.
@ -82,6 +82,7 @@ accepted_design:
- `thread/epiphany/jobsUpdated` is the first live bound-job progress notification: it translates runtime `agent_job_progress:{json}` background events into changed launcher-bound payloads by resolving durable `job_bindings` against live `agent_jobs` snapshots through the current backend adapter, without polling in a loop, mutating state, or scheduling work.
- `thread/epiphany/freshness` is the first read-only freshness surface: it derives retrieval and graph staleness from live retrieval summaries plus frontier/churn state, and for loaded threads reflects watcher-backed invalidation inputs with changed paths plus mapped graph/frontier hits, without mutating, scheduling, or performing automatic semantic invalidation.
- `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.
- `thread/epiphany/graphQuery` is the first read-only graph traversal surface over authoritative typed state: it returns explicit nodes, path/symbol matches, bounded neighbors, and frontier neighborhoods with architecture/dataflow links without mutating, notifying, indexing, scheduling, proposing, or promoting.
- `thread/epiphany/pressure` is the first read-only context-pressure surface: it derives pressure from token telemetry and auto-compact/context limits without compacting, scheduling, mutating, or notifying.
- `thread/epiphany/reorient` is the first bounded CRRC policy surface: it derives a read-only `resume` versus `regather` verdict from checkpoint, freshness, watcher, and pressure signals without mutating state, compacting, scheduling, or silently continuing work.
- `thread/epiphany/crrc` is the first bounded CRRC coordinator recommendation surface: it composes pressure, the reorientation verdict, fixed reorient-worker binding/result status, finding read-back, and available actions into one read-only recommendation without launching, accepting, compacting, scheduling, or mutating state.
@ -117,7 +118,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, freshness with watcher-backed invalidation inputs, targeted context, context-pressure, reorientation policy, and CRRC coordinator recommendation surfaces, plus durable investigation checkpointing for compaction-safe planning, live bound-job progress notifications, a thin launcher seam in durable job bindings, explicit launch/interrupt authority over that seam, and a bounded reorient-guided worker launch surface.
- Phase 6: read-only scene, job/progress, freshness with watcher-backed invalidation inputs, targeted context, graph traversal, context-pressure, reorientation policy, and CRRC coordinator recommendation surfaces, plus durable investigation checkpointing for compaction-safe planning, live bound-job progress notifications, a thin launcher seam in durable job bindings, explicit launch/interrupt authority over that seam, and a bounded reorient-guided worker launch surface.
- Phase 6 read-back: read-only reorient-worker result reflection through `thread/epiphany/reorientResult`.
- Phase 6 acceptance: explicit reorient-worker finding acceptance through `thread/epiphany/reorientAccept`.
- Phase 6 dogfood view: first CLI operator status over scene/pressure/reorient/jobs/result/CRRC.
@ -137,6 +138,7 @@ landed_baseline:
- Use `tools/epiphany_phase6_freshness_smoke.py` before modifying freshness reflection behavior.
- Use `tools/epiphany_phase6_invalidation_smoke.py` before modifying watcher-backed invalidation behavior inside freshness reflection.
- Use `tools/epiphany_phase6_context_smoke.py` before modifying targeted context-shard behavior.
- Use `tools/epiphany_phase6_graph_query_smoke.py` before modifying graph traversal behavior.
- Use `tools/epiphany_phase6_pressure_smoke.py` before modifying context-pressure reflection behavior.
- Use `tools/epiphany_phase6_reorient_smoke.py` before modifying bounded CRRC reorientation policy behavior.
- Use `tools/epiphany_phase6_reorient_launch_smoke.py` before modifying the bounded reorient-guided worker launch surface.
@ -165,5 +167,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, roles, freshness, targeted context, pressure, reorient, CRRC, coordinator, reorient launch/result/accept, explicit modeling/checkpoint and verification/review role launch/result templates, MVP status, dogfood runner, coordinator runner, live specialist runner, limited safe-boundary CRRC automation, pre-compaction checkpoint intervention, and the first Tauri/React GUI operator shell are landed. Native CRRC can steer active loaded Epiphany turns once at `shouldPrepareCompaction`; turn-complete automation can still compact or launch the fixed reorient-worker when the coordinator says it can auto-run. The GUI now has desktop/mobile visual smoke, durable checkpoint preparation, bounded status/coordinator artifact buttons, fixed role/reorient launch and read-back buttons, and explicit reorientAccept after a completed result. Implementation remains main-agent work; CRRC remains workflow automation, not a specialist persona; GUI remains an operator surface over typed state, not a source of truth.
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, roles, freshness, targeted context, graph traversal, pressure, reorient, CRRC, coordinator, reorient launch/result/accept, explicit modeling/checkpoint and verification/review role launch/result templates, MVP status, dogfood runner, coordinator runner, live specialist runner, limited safe-boundary CRRC automation, pre-compaction checkpoint intervention, and the first Tauri/React GUI operator shell are landed. Native CRRC can steer active loaded Epiphany turns once at `shouldPrepareCompaction`; turn-complete automation can still compact or launch the fixed reorient-worker when the coordinator says it can auto-run. The GUI now has desktop/mobile visual smoke, durable checkpoint preparation, bounded status/coordinator artifact buttons, fixed role/reorient launch and read-back buttons, and explicit reorientAccept after a completed result. Implementation remains main-agent work; CRRC remains workflow automation, not a specialist persona; GUI remains an operator surface over typed state, not a source of truth.
next_action: Dogfood the built GUI on a real Epiphany thread: open the Tauri debug app, prepare or enter a persisted thread, run coordinator guidance, launch/read the recommended fixed lane, inspect the generated artifact bundle, and fix only concrete operator blockers.

View file

@ -0,0 +1,324 @@
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
from epiphany_phase5_smoke import AppServerClient
from epiphany_phase5_smoke import DEFAULT_APP_SERVER
from epiphany_phase5_smoke import ROOT
from epiphany_phase5_smoke import require
from epiphany_phase5_smoke import reset_smoke_paths
DEFAULT_CODEX_HOME = ROOT / ".epiphany-smoke" / "phase6-graph-query-codex-home"
DEFAULT_RESULT = ROOT / ".epiphany-smoke" / "phase6-graph-query-smoke-result.json"
DEFAULT_TRANSCRIPT = ROOT / ".epiphany-smoke" / "phase6-graph-query-smoke-transcript.jsonl"
DEFAULT_STDERR = ROOT / ".epiphany-smoke" / "phase6-graph-query-smoke-server.stderr.log"
GRAPH_QUERY_CODE_REF = {
"path": "app-server/src/codex_message_processor.rs",
"start_line": 15201,
"end_line": 15534,
"symbol": "map_epiphany_graph_query",
}
def graph_query_patch() -> dict[str, Any]:
return {
"objective": "Expose read-only graph traversal for implementation and verifier agents.",
"activeSubgoalId": "phase6-graph-query-smoke",
"subgoals": [
{
"id": "phase6-graph-query-smoke",
"title": "Live-smoke graph query traversal",
"status": "active",
"summary": "The graph query surface should traverse typed graph state without mutating it.",
}
],
"graphs": {
"architecture": {
"nodes": [
{
"id": "graph-query-surface",
"title": "Graph query surface",
"purpose": "Return bounded graph neighborhoods and path matches for agents.",
"code_refs": [GRAPH_QUERY_CODE_REF],
},
{
"id": "implementation-agent",
"title": "Implementation agent",
"purpose": "Uses graph neighborhoods to decide where to edit.",
},
{
"id": "verifier-agent",
"title": "Verifier agent",
"purpose": "Uses graph neighborhoods to inspect blast radius.",
},
],
"edges": [
{
"id": "edge-query-implementation",
"source_id": "graph-query-surface",
"target_id": "implementation-agent",
"kind": "guides",
"code_refs": [GRAPH_QUERY_CODE_REF],
},
{
"id": "edge-query-verifier",
"source_id": "graph-query-surface",
"target_id": "verifier-agent",
"kind": "guides",
},
],
},
"dataflow": {
"nodes": [
{
"id": "typed-graph-state",
"title": "Typed graph state",
"purpose": "Remain the authoritative model behind graph traversal.",
"code_refs": [GRAPH_QUERY_CODE_REF],
}
]
},
"links": [
{
"dataflow_node_id": "typed-graph-state",
"architecture_node_id": "graph-query-surface",
"relationship": "authoritative-query-source",
"code_refs": [GRAPH_QUERY_CODE_REF],
}
],
},
"graphFrontier": {
"active_node_ids": ["graph-query-surface"],
"active_edge_ids": ["edge-query-implementation"],
},
"graphCheckpoint": {
"checkpoint_id": "phase6-graph-query-smoke",
"graph_revision": 1,
"summary": "Graph query traversal is the active Phase 6 smoke target.",
"frontier_node_ids": ["graph-query-surface"],
},
}
def assert_missing_graph_query(response: dict[str, Any], thread_id: str) -> None:
require(response["threadId"] == thread_id, "graph query should echo thread id")
require(response["source"] == "live", "missing graph query should report live source")
require(response["stateStatus"] == "missing", "missing graph query should report missing state")
require("stateRevision" not in response, "missing graph query should not invent a revision")
require(response["graph"] == {}, "missing graph query should not invent graph records")
require(
response["missing"]["nodeIds"] == ["graph-query-surface"],
"missing graph query should echo unresolved explicit node ids",
)
def assert_frontier_graph_query(response: dict[str, Any]) -> None:
require(response["stateStatus"] == "ready", "frontier graph query should report ready state")
require(response["stateRevision"] == 1, "frontier graph query should preserve revision")
require(
[node["id"] for node in response["graph"]["architectureNodes"]]
== ["graph-query-surface", "implementation-agent", "verifier-agent"],
"frontier graph query should return one-hop architecture neighbors",
)
require(
[node["id"] for node in response["graph"]["dataflowNodes"]] == ["typed-graph-state"],
"frontier graph query should preserve linked dataflow node",
)
require(
[edge["id"] for edge in response["graph"]["architectureEdges"]]
== ["edge-query-implementation", "edge-query-verifier"],
"frontier graph query should return active and incident architecture edges",
)
require(
response["graph"]["links"][0]["dataflow_node_id"] == "typed-graph-state",
"frontier graph query should return architecture/dataflow link",
)
require(
response["frontier"]["active_node_ids"] == ["graph-query-surface"],
"frontier graph query should include current frontier",
)
require(
response["checkpoint"]["checkpoint_id"] == "phase6-graph-query-smoke",
"frontier graph query should include graph checkpoint",
)
require(response["missing"] == {}, "frontier graph query should have no missing records")
def assert_path_graph_query(response: dict[str, Any]) -> None:
require(response["stateStatus"] == "ready", "path graph query should report ready state")
require(
response["matched"]["paths"] == ["app-server/src/codex_message_processor.rs"],
"path graph query should report matched code ref path",
)
require(
response["matched"]["symbols"] == ["map_epiphany_graph_query"],
"path graph query should report matched symbol",
)
require(
"graph-query-surface" in response["matched"]["nodeIds"],
"path graph query should include graph query node in matches",
)
require(
"typed-graph-state" in response["matched"]["nodeIds"],
"path graph query should include linked dataflow node in matches",
)
def run_smoke(args: argparse.Namespace) -> dict[str, Any]:
app_server = args.app_server.resolve()
if not app_server.exists():
raise FileNotFoundError(f"codex app-server binary not found: {app_server}")
codex_home = args.codex_home.resolve()
result_path = args.result.resolve()
transcript_path = args.transcript.resolve()
stderr_path = args.stderr.resolve()
reset_smoke_paths(codex_home, result_path, transcript_path, stderr_path)
with AppServerClient(app_server, codex_home, transcript_path, stderr_path) as client:
client.send(
"initialize",
{
"clientInfo": {
"name": "epiphany-phase6-graph-query-smoke",
"title": "Epiphany Phase 6 Graph Query Smoke",
"version": "0.1.0",
},
"capabilities": {"experimentalApi": True},
},
)
client.send("initialized", expect_response=False)
started = client.send(
"thread/start",
{"cwd": str(ROOT / "epiphany-core"), "ephemeral": True},
)
assert started is not None
thread_id = started["thread"]["id"]
missing_notification_start = len(client.notifications)
missing_response = client.send(
"thread/epiphany/graphQuery",
{
"threadId": thread_id,
"query": {"kind": "node", "nodeIds": ["graph-query-surface"]},
},
)
assert missing_response is not None
assert_missing_graph_query(missing_response, thread_id)
client.require_no_notification(
"thread/epiphany/stateUpdated",
start_index=missing_notification_start,
)
update_notification_start = len(client.notifications)
update = client.send(
"thread/epiphany/update",
{"threadId": thread_id, "expectedRevision": 0, "patch": graph_query_patch()},
)
assert update is not None
require(update["revision"] == 1, "graph query smoke patch should advance revision to 1")
client.wait_for_notification(
"thread/epiphany/stateUpdated",
start_index=update_notification_start,
)
frontier_notification_start = len(client.notifications)
frontier_response = client.send(
"thread/epiphany/graphQuery",
{
"threadId": thread_id,
"query": {
"kind": "frontierNeighborhood",
"direction": "outgoing",
"depth": 1,
},
},
)
assert frontier_response is not None
assert_frontier_graph_query(frontier_response)
client.require_no_notification(
"thread/epiphany/stateUpdated",
start_index=frontier_notification_start,
)
path_notification_start = len(client.notifications)
path_response = client.send(
"thread/epiphany/graphQuery",
{
"threadId": thread_id,
"query": {
"kind": "path",
"paths": ["app-server/src/codex_message_processor.rs"],
"symbols": ["map_epiphany_graph_query"],
},
},
)
assert path_response is not None
assert_path_graph_query(path_response)
client.require_no_notification(
"thread/epiphany/stateUpdated",
start_index=path_notification_start,
)
final_read = client.send("thread/read", {"threadId": thread_id, "includeTurns": False})
assert final_read is not None
require(
final_read["thread"]["epiphanyState"]["revision"] == 1,
"graph query reflection should not mutate state revision",
)
result = {
"threadId": thread_id,
"codexHome": str(codex_home),
"missingStateStatus": missing_response["stateStatus"],
"frontierStateStatus": frontier_response["stateStatus"],
"pathStateStatus": path_response["stateStatus"],
"readyRevision": frontier_response["stateRevision"],
"frontierArchitectureNodeIds": [
node["id"] for node in frontier_response["graph"]["architectureNodes"]
],
"frontierDataflowNodeIds": [
node["id"] for node in frontier_response["graph"]["dataflowNodes"]
],
"pathMatchedSymbols": path_response["matched"]["symbols"],
"frontierNotificationCount": client.count_notifications(
"thread/epiphany/stateUpdated",
start_index=frontier_notification_start,
),
"pathNotificationCount": client.count_notifications(
"thread/epiphany/stateUpdated",
start_index=path_notification_start,
),
"finalReadRevision": final_read["thread"]["epiphanyState"]["revision"],
}
result_path.parent.mkdir(parents=True, exist_ok=True)
result_path.write_text(
json.dumps(result, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
return result
def main() -> int:
parser = argparse.ArgumentParser(
description="Live-smoke the Phase 6 Epiphany graph query traversal surface."
)
parser.add_argument("--app-server", type=Path, default=DEFAULT_APP_SERVER)
parser.add_argument("--codex-home", type=Path, default=DEFAULT_CODEX_HOME)
parser.add_argument("--result", type=Path, default=DEFAULT_RESULT)
parser.add_argument("--transcript", type=Path, default=DEFAULT_TRANSCRIPT)
parser.add_argument("--stderr", type=Path, default=DEFAULT_STDERR)
args = parser.parse_args()
result = run_smoke(args)
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -149,11 +149,17 @@ def assert_missing_scene(scene: dict[str, Any]) -> None:
"retrieve",
"distill",
"context",
"graphQuery",
"jobs",
"roles",
"coordinator",
"roleLaunch",
"roleResult",
"jobLaunch",
"freshness",
"pressure",
"reorient",
"crrc",
"update",
],
"missing live scene should expose only bootstrap actions",
@ -239,11 +245,17 @@ def assert_ready_scene(scene: dict[str, Any], expected_revision: int) -> None:
"retrieve",
"distill",
"context",
"graphQuery",
"jobs",
"roles",
"coordinator",
"roleLaunch",
"roleResult",
"jobLaunch",
"freshness",
"pressure",
"reorient",
"crrc",
"reorientLaunch",
"update",
"jobInterrupt",

View file

@ -3872,6 +3872,91 @@
],
"type": "object"
},
"ThreadEpiphanyGraphQuery": {
"properties": {
"depth": {
"description": "Defaults to 1 and is capped by the server.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"direction": {
"anyOf": [
{
"$ref": "#/definitions/ThreadEpiphanyGraphQueryDirection"
},
{
"type": "null"
}
],
"description": "Defaults to `both`."
},
"edgeIds": {
"items": {
"type": "string"
},
"type": "array"
},
"edgeKinds": {
"items": {
"type": "string"
},
"type": "array"
},
"includeLinks": {
"description": "Defaults to true so neighborhood queries preserve dataflow/architecture joins.",
"type": [
"boolean",
"null"
]
},
"kind": {
"$ref": "#/definitions/ThreadEpiphanyGraphQueryKind"
},
"nodeIds": {
"items": {
"type": "string"
},
"type": "array"
},
"paths": {
"items": {
"type": "string"
},
"type": "array"
},
"symbols": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"kind"
],
"type": "object"
},
"ThreadEpiphanyGraphQueryDirection": {
"enum": [
"incoming",
"outgoing",
"both"
],
"type": "string"
},
"ThreadEpiphanyGraphQueryKind": {
"enum": [
"node",
"path",
"frontierNeighborhood",
"neighbors"
],
"type": "string"
},
"ThreadEpiphanyRoleId": {
"enum": [
"implementation",

View file

@ -15294,6 +15294,143 @@
],
"type": "string"
},
"ThreadEpiphanyGraphQuery": {
"properties": {
"depth": {
"description": "Defaults to 1 and is capped by the server.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"direction": {
"anyOf": [
{
"$ref": "#/definitions/v2/ThreadEpiphanyGraphQueryDirection"
},
{
"type": "null"
}
],
"description": "Defaults to `both`."
},
"edgeIds": {
"items": {
"type": "string"
},
"type": "array"
},
"edgeKinds": {
"items": {
"type": "string"
},
"type": "array"
},
"includeLinks": {
"description": "Defaults to true so neighborhood queries preserve dataflow/architecture joins.",
"type": [
"boolean",
"null"
]
},
"kind": {
"$ref": "#/definitions/v2/ThreadEpiphanyGraphQueryKind"
},
"nodeIds": {
"items": {
"type": "string"
},
"type": "array"
},
"paths": {
"items": {
"type": "string"
},
"type": "array"
},
"symbols": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"kind"
],
"type": "object"
},
"ThreadEpiphanyGraphQueryDirection": {
"enum": [
"incoming",
"outgoing",
"both"
],
"type": "string"
},
"ThreadEpiphanyGraphQueryKind": {
"enum": [
"node",
"path",
"frontierNeighborhood",
"neighbors"
],
"type": "string"
},
"ThreadEpiphanyGraphQueryMatched": {
"properties": {
"edgeIds": {
"items": {
"type": "string"
},
"type": "array"
},
"edgeKinds": {
"items": {
"type": "string"
},
"type": "array"
},
"nodeIds": {
"items": {
"type": "string"
},
"type": "array"
},
"paths": {
"items": {
"type": "string"
},
"type": "array"
},
"symbols": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"ThreadEpiphanyGraphQueryMissing": {
"properties": {
"edgeIds": {
"items": {
"type": "string"
},
"type": "array"
},
"nodeIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"ThreadEpiphanyInvalidationInput": {
"properties": {
"activeFrontierNodeIds": {
@ -16309,6 +16446,7 @@
"retrieve",
"distill",
"context",
"graphQuery",
"jobs",
"roles",
"coordinator",

View file

@ -13181,6 +13181,143 @@
],
"type": "string"
},
"ThreadEpiphanyGraphQuery": {
"properties": {
"depth": {
"description": "Defaults to 1 and is capped by the server.",
"format": "uint32",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"direction": {
"anyOf": [
{
"$ref": "#/definitions/ThreadEpiphanyGraphQueryDirection"
},
{
"type": "null"
}
],
"description": "Defaults to `both`."
},
"edgeIds": {
"items": {
"type": "string"
},
"type": "array"
},
"edgeKinds": {
"items": {
"type": "string"
},
"type": "array"
},
"includeLinks": {
"description": "Defaults to true so neighborhood queries preserve dataflow/architecture joins.",
"type": [
"boolean",
"null"
]
},
"kind": {
"$ref": "#/definitions/ThreadEpiphanyGraphQueryKind"
},
"nodeIds": {
"items": {
"type": "string"
},
"type": "array"
},
"paths": {
"items": {
"type": "string"
},
"type": "array"
},
"symbols": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"kind"
],
"type": "object"
},
"ThreadEpiphanyGraphQueryDirection": {
"enum": [
"incoming",
"outgoing",
"both"
],
"type": "string"
},
"ThreadEpiphanyGraphQueryKind": {
"enum": [
"node",
"path",
"frontierNeighborhood",
"neighbors"
],
"type": "string"
},
"ThreadEpiphanyGraphQueryMatched": {
"properties": {
"edgeIds": {
"items": {
"type": "string"
},
"type": "array"
},
"edgeKinds": {
"items": {
"type": "string"
},
"type": "array"
},
"nodeIds": {
"items": {
"type": "string"
},
"type": "array"
},
"paths": {
"items": {
"type": "string"
},
"type": "array"
},
"symbols": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"ThreadEpiphanyGraphQueryMissing": {
"properties": {
"edgeIds": {
"items": {
"type": "string"
},
"type": "array"
},
"nodeIds": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"ThreadEpiphanyInvalidationInput": {
"properties": {
"activeFrontierNodeIds": {
@ -14196,6 +14333,7 @@
"retrieve",
"distill",
"context",
"graphQuery",
"jobs",
"roles",
"coordinator",

View file

@ -0,0 +1,19 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ThreadEpiphanyGraphQueryDirection } from "./ThreadEpiphanyGraphQueryDirection";
import type { ThreadEpiphanyGraphQueryKind } from "./ThreadEpiphanyGraphQueryKind";
export type ThreadEpiphanyGraphQuery = { kind: ThreadEpiphanyGraphQueryKind, nodeIds?: Array<string>, edgeIds?: Array<string>, paths?: Array<string>, symbols?: Array<string>, edgeKinds?: Array<string>,
/**
* Defaults to `both`.
*/
direction?: ThreadEpiphanyGraphQueryDirection | null,
/**
* Defaults to 1 and is capped by the server.
*/
depth?: number | null,
/**
* Defaults to true so neighborhood queries preserve dataflow/architecture joins.
*/
includeLinks?: boolean | null, };

View file

@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ThreadEpiphanyGraphQueryDirection = "incoming" | "outgoing" | "both";

View file

@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ThreadEpiphanyGraphQueryKind = "node" | "path" | "frontierNeighborhood" | "neighbors";

View file

@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ThreadEpiphanyGraphQueryMatched = { nodeIds?: Array<string>, edgeIds?: Array<string>, paths?: Array<string>, symbols?: Array<string>, edgeKinds?: Array<string>, };

View file

@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ThreadEpiphanyGraphQueryMissing = { nodeIds?: Array<string>, edgeIds?: Array<string>, };

View file

@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ThreadEpiphanySceneAction = "index" | "retrieve" | "distill" | "context" | "jobs" | "roles" | "coordinator" | "roleLaunch" | "roleResult" | "jobLaunch" | "jobInterrupt" | "freshness" | "pressure" | "reorient" | "crrc" | "reorientLaunch" | "reorientResult" | "reorientAccept" | "propose" | "promote" | "update";
export type ThreadEpiphanySceneAction = "index" | "retrieve" | "distill" | "context" | "graphQuery" | "jobs" | "roles" | "coordinator" | "roleLaunch" | "roleResult" | "jobLaunch" | "jobInterrupt" | "freshness" | "pressure" | "reorient" | "crrc" | "reorientLaunch" | "reorientResult" | "reorientAccept" | "propose" | "promote" | "update";

View file

@ -329,6 +329,11 @@ export type { ThreadEpiphanyFreshnessSource } from "./ThreadEpiphanyFreshnessSou
export type { ThreadEpiphanyGraphContext } from "./ThreadEpiphanyGraphContext";
export type { ThreadEpiphanyGraphFreshness } from "./ThreadEpiphanyGraphFreshness";
export type { ThreadEpiphanyGraphFreshnessStatus } from "./ThreadEpiphanyGraphFreshnessStatus";
export type { ThreadEpiphanyGraphQuery } from "./ThreadEpiphanyGraphQuery";
export type { ThreadEpiphanyGraphQueryDirection } from "./ThreadEpiphanyGraphQueryDirection";
export type { ThreadEpiphanyGraphQueryKind } from "./ThreadEpiphanyGraphQueryKind";
export type { ThreadEpiphanyGraphQueryMatched } from "./ThreadEpiphanyGraphQueryMatched";
export type { ThreadEpiphanyGraphQueryMissing } from "./ThreadEpiphanyGraphQueryMissing";
export type { ThreadEpiphanyInvalidationInput } from "./ThreadEpiphanyInvalidationInput";
export type { ThreadEpiphanyInvalidationStatus } from "./ThreadEpiphanyInvalidationStatus";
export type { ThreadEpiphanyJob } from "./ThreadEpiphanyJob";

View file

@ -376,6 +376,11 @@ client_request_definitions! {
params: v2::ThreadEpiphanyContextParams,
response: v2::ThreadEpiphanyContextResponse,
},
#[experimental("thread/epiphany/graphQuery")]
ThreadEpiphanyGraphQuery => "thread/epiphany/graphQuery" {
params: v2::ThreadEpiphanyGraphQueryParams,
response: v2::ThreadEpiphanyGraphQueryResponse,
},
#[experimental("thread/epiphany/pressure")]
ThreadEpiphanyPressure => "thread/epiphany/pressure" {
params: v2::ThreadEpiphanyPressureParams,
@ -2540,6 +2545,104 @@ mod tests {
Ok(())
}
#[test]
fn serialize_thread_epiphany_graph_query_request_and_response() -> Result<()> {
let request = ClientRequest::ThreadEpiphanyGraphQuery {
request_id: RequestId::Integer(24),
params: v2::ThreadEpiphanyGraphQueryParams {
thread_id: "thr_123".to_string(),
query: v2::ThreadEpiphanyGraphQuery {
kind: v2::ThreadEpiphanyGraphQueryKind::FrontierNeighborhood,
node_ids: vec!["state-spine".to_string()],
edge_ids: Vec::new(),
paths: vec![PathBuf::from("src/lib.rs")],
symbols: Vec::new(),
edge_kinds: vec!["calls".to_string()],
direction: Some(v2::ThreadEpiphanyGraphQueryDirection::Both),
depth: Some(1),
include_links: Some(true),
},
},
};
assert_eq!(request.method(), "thread/epiphany/graphQuery");
assert_eq!(
json!({
"method": "thread/epiphany/graphQuery",
"id": 24,
"params": {
"threadId": "thr_123",
"query": {
"kind": "frontierNeighborhood",
"nodeIds": ["state-spine"],
"paths": ["src/lib.rs"],
"edgeKinds": ["calls"],
"direction": "both",
"depth": 1,
"includeLinks": true
}
}
}),
serde_json::to_value(&request)?,
);
let response = ClientResponse::ThreadEpiphanyGraphQuery {
request_id: RequestId::Integer(24),
response: v2::ThreadEpiphanyGraphQueryResponse {
thread_id: "thr_123".to_string(),
source: v2::ThreadEpiphanyContextSource::Live,
state_status: v2::ThreadEpiphanyContextStateStatus::Ready,
state_revision: Some(2),
graph: v2::ThreadEpiphanyGraphContext {
architecture_nodes: vec![codex_protocol::protocol::EpiphanyGraphNode {
id: "state-spine".to_string(),
title: "State spine".to_string(),
purpose: "Persist Epiphany state.".to_string(),
..Default::default()
}],
..Default::default()
},
frontier: None,
checkpoint: None,
matched: v2::ThreadEpiphanyGraphQueryMatched {
node_ids: vec!["state-spine".to_string()],
edge_ids: Vec::new(),
paths: Vec::new(),
symbols: Vec::new(),
edge_kinds: Vec::new(),
},
missing: v2::ThreadEpiphanyGraphQueryMissing::default(),
},
};
assert_eq!(response.method(), "thread/epiphany/graphQuery");
assert_eq!(
json!({
"method": "thread/epiphany/graphQuery",
"id": 24,
"response": {
"threadId": "thr_123",
"source": "live",
"stateStatus": "ready",
"stateRevision": 2,
"graph": {
"architectureNodes": [{
"id": "state-spine",
"title": "State spine",
"purpose": "Persist Epiphany state."
}]
},
"matched": {
"nodeIds": ["state-spine"]
},
"missing": {}
}
}),
serde_json::to_value(&response)?,
);
Ok(())
}
#[test]
fn serialize_thread_epiphany_pressure_response() -> Result<()> {
let response = ClientResponse::ThreadEpiphanyPressure {

View file

@ -3996,6 +3996,7 @@ pub enum ThreadEpiphanySceneAction {
Retrieve,
Distill,
Context,
GraphQuery,
Jobs,
Roles,
Coordinator,
@ -4713,6 +4714,109 @@ pub struct ThreadEpiphanyContextMissing {
pub evidence_ids: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadEpiphanyGraphQueryParams {
pub thread_id: String,
pub query: ThreadEpiphanyGraphQuery,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadEpiphanyGraphQuery {
pub kind: ThreadEpiphanyGraphQueryKind,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub node_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edge_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub paths: Vec<PathBuf>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub symbols: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edge_kinds: Vec<String>,
/// Defaults to `both`.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub direction: Option<ThreadEpiphanyGraphQueryDirection>,
/// Defaults to 1 and is capped by the server.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub depth: Option<u32>,
/// Defaults to true so neighborhood queries preserve dataflow/architecture joins.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub include_links: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub enum ThreadEpiphanyGraphQueryKind {
Node,
Path,
FrontierNeighborhood,
Neighbors,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub enum ThreadEpiphanyGraphQueryDirection {
Incoming,
Outgoing,
Both,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadEpiphanyGraphQueryResponse {
pub thread_id: String,
pub source: ThreadEpiphanyContextSource,
pub state_status: ThreadEpiphanyContextStateStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub state_revision: Option<u64>,
pub graph: ThreadEpiphanyGraphContext,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub frontier: Option<CoreEpiphanyGraphFrontier>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub checkpoint: Option<CoreEpiphanyGraphCheckpoint>,
pub matched: ThreadEpiphanyGraphQueryMatched,
pub missing: ThreadEpiphanyGraphQueryMissing,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS, Default)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadEpiphanyGraphQueryMatched {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub node_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edge_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub paths: Vec<PathBuf>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub symbols: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edge_kinds: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS, Default)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadEpiphanyGraphQueryMissing {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub node_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edge_ids: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]

View file

@ -143,6 +143,8 @@ Experimental Epiphany retrieval:
- `thread/epiphany/index` runs an explicit loaded-thread-only semantic indexing pass that writes a Qdrant-backed workspace index under Codex home metadata, using local Ollama embeddings by default. It is the write path for persistent semantic retrieval and keeps `thread/epiphany/retrieve` read-only.
- `thread/epiphany/retrieve` runs a loaded-thread-only hybrid repo query that combines `codex_file_search` path hits with BM25 chunk retrieval and returns typed result metadata plus an Epiphany retrieval summary.
- `thread/epiphany/distill` is a loaded-thread-only read surface that turns one explicit observation input into a deterministic observation/evidence patch plus the current `expectedRevision`. It does not mutate thread state; callers must pass the returned patch to `thread/epiphany/update` to promote it.
- `thread/epiphany/context` is a 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.
- `thread/epiphany/graphQuery` is a read-only graph traversal surface over authoritative typed state. It can return explicit nodes, path/symbol matches, one-to-three-hop neighbors, and the current frontier neighborhood while preserving architecture/dataflow links; it does not mutate, index, schedule, or promote.
- `thread/epiphany/promote` is an explicit loaded-thread-only promotion gate. It evaluates a patch plus verifier evidence, rejects with reasons without mutating state when policy fails, and applies the patch through the durable update path when accepted. Accepted responses include the landed `revision` and `changedFields`.
- `thread/epiphany/update` is the explicit loaded-thread-only control-plane write path for Epiphany state. It can append observations/evidence and replace bounded typed state fields such as objective, subgoals, graph frontier/checkpoint, scratch, churn, and mode; it bumps the Epiphany revision, returns `revision` plus `changedFields`, and persists a rollout snapshot immediately.
- `thread/epiphany/stateUpdated` is an experimental server notification emitted after successful `thread/epiphany/update` and accepted `thread/epiphany/promote`. It carries the updated typed `epiphanyState`, a `source` of `update` or `promote`, event-level `revision`, and typed `changedFields`; rejected promotions do not emit it.

View file

@ -170,6 +170,13 @@ use codex_app_server_protocol::ThreadEpiphanyFreshnessSource;
use codex_app_server_protocol::ThreadEpiphanyGraphContext;
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::ThreadEpiphanyGraphQueryMatched;
use codex_app_server_protocol::ThreadEpiphanyGraphQueryMissing;
use codex_app_server_protocol::ThreadEpiphanyGraphQueryParams;
use codex_app_server_protocol::ThreadEpiphanyGraphQueryResponse;
use codex_app_server_protocol::ThreadEpiphanyIndexParams;
use codex_app_server_protocol::ThreadEpiphanyIndexResponse;
use codex_app_server_protocol::ThreadEpiphanyInvalidationInput;
@ -1126,6 +1133,10 @@ impl CodexMessageProcessor {
self.thread_epiphany_context(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::ThreadEpiphanyGraphQuery { request_id, params } => {
self.thread_epiphany_graph_query(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::ThreadEpiphanyPressure { request_id, params } => {
self.thread_epiphany_pressure(to_connection_request_id(request_id), params)
.await;
@ -5043,6 +5054,54 @@ impl CodexMessageProcessor {
self.outgoing.send_response(request_id, response).await;
}
async fn thread_epiphany_graph_query(
&self,
request_id: ConnectionRequestId,
params: ThreadEpiphanyGraphQueryParams,
) {
let thread_id = params.thread_id.clone();
let thread_uuid = match ThreadId::from_string(&thread_id) {
Ok(id) => id,
Err(err) => {
self.send_invalid_request_error(request_id, format!("invalid thread id: {err}"))
.await;
return;
}
};
let loaded = self.thread_manager.get_thread(thread_uuid).await.is_ok();
let thread = match self.read_thread_view(thread_uuid, false).await {
Ok(thread) => thread,
Err(ThreadReadViewError::InvalidRequest(message)) => {
self.send_invalid_request_error(request_id, message).await;
return;
}
Err(ThreadReadViewError::Internal(message)) => {
self.send_internal_error(request_id, message).await;
return;
}
};
let (state_status, state_revision, graph, frontier, checkpoint, matched, missing) =
map_epiphany_graph_query(thread.epiphany_state.as_ref(), &params.query);
let response = ThreadEpiphanyGraphQueryResponse {
thread_id,
source: if loaded {
ThreadEpiphanyContextSource::Live
} else {
ThreadEpiphanyContextSource::Stored
},
state_status,
state_revision,
graph,
frontier,
checkpoint,
matched,
missing,
};
self.outgoing.send_response(request_id, response).await;
}
async fn thread_epiphany_pressure(
&self,
request_id: ConnectionRequestId,
@ -12705,6 +12764,7 @@ fn epiphany_scene_available_actions(
ThreadEpiphanySceneAction::Retrieve,
ThreadEpiphanySceneAction::Distill,
ThreadEpiphanySceneAction::Context,
ThreadEpiphanySceneAction::GraphQuery,
ThreadEpiphanySceneAction::Jobs,
ThreadEpiphanySceneAction::Roles,
ThreadEpiphanySceneAction::Coordinator,
@ -15138,6 +15198,348 @@ fn map_epiphany_context(
)
}
fn map_epiphany_graph_query(
state: Option<&EpiphanyThreadState>,
query: &ThreadEpiphanyGraphQuery,
) -> (
ThreadEpiphanyContextStateStatus,
Option<u64>,
ThreadEpiphanyGraphContext,
Option<codex_protocol::protocol::EpiphanyGraphFrontier>,
Option<codex_protocol::protocol::EpiphanyGraphCheckpoint>,
ThreadEpiphanyGraphQueryMatched,
ThreadEpiphanyGraphQueryMissing,
) {
let Some(state) = state else {
return (
ThreadEpiphanyContextStateStatus::Missing,
None,
ThreadEpiphanyGraphContext::default(),
None,
None,
ThreadEpiphanyGraphQueryMatched::default(),
ThreadEpiphanyGraphQueryMissing {
node_ids: unique_strings(query.node_ids.iter().cloned()),
edge_ids: unique_strings(query.edge_ids.iter().cloned()),
},
);
};
let mut node_ids = unique_strings(query.node_ids.iter().cloned());
let mut edge_ids = unique_strings(query.edge_ids.iter().cloned());
let mut matched_paths: Vec<PathBuf> = Vec::new();
let mut matched_symbols: Vec<String> = Vec::new();
let mut matched_edge_kinds: Vec<String> = Vec::new();
match query.kind {
ThreadEpiphanyGraphQueryKind::Node => {}
ThreadEpiphanyGraphQueryKind::Path => {
collect_graph_query_path_matches(
state,
query,
&mut node_ids,
&mut edge_ids,
&mut matched_paths,
&mut matched_symbols,
);
}
ThreadEpiphanyGraphQueryKind::FrontierNeighborhood => {
if let Some(frontier) = state.graph_frontier.as_ref() {
extend_unique_strings(&mut node_ids, frontier.active_node_ids.iter().cloned());
extend_unique_strings(&mut edge_ids, frontier.active_edge_ids.iter().cloned());
}
expand_graph_query_neighbors(state, query, &mut node_ids, &mut edge_ids);
}
ThreadEpiphanyGraphQueryKind::Neighbors => {
expand_graph_query_neighbors(state, query, &mut node_ids, &mut edge_ids);
}
}
collect_graph_query_edge_kind_matches(
state,
query,
&mut node_ids,
&mut edge_ids,
&mut matched_edge_kinds,
);
if query.include_links.unwrap_or(true) {
expand_graph_query_links(state, &mut node_ids);
}
let node_id_set: HashSet<&str> = node_ids.iter().map(String::as_str).collect();
let edge_id_set: HashSet<&str> = edge_ids.iter().map(String::as_str).collect();
let architecture_nodes = state
.graphs
.architecture
.nodes
.iter()
.filter(|node| node_id_set.contains(node.id.as_str()))
.cloned()
.collect::<Vec<_>>();
let dataflow_nodes = state
.graphs
.dataflow
.nodes
.iter()
.filter(|node| node_id_set.contains(node.id.as_str()))
.cloned()
.collect::<Vec<_>>();
let found_node_ids: HashSet<String> = architecture_nodes
.iter()
.chain(dataflow_nodes.iter())
.map(|node| node.id.clone())
.collect();
let matched_node_ids = architecture_nodes
.iter()
.chain(dataflow_nodes.iter())
.map(|node| node.id.clone())
.collect::<Vec<_>>();
let architecture_edges =
select_epiphany_graph_edges(&state.graphs.architecture.edges, &node_id_set, &edge_id_set);
let dataflow_edges =
select_epiphany_graph_edges(&state.graphs.dataflow.edges, &node_id_set, &edge_id_set);
let found_edge_ids: HashSet<String> = architecture_edges
.iter()
.chain(dataflow_edges.iter())
.filter_map(|edge| edge.id.clone())
.collect();
let matched_edge_ids = architecture_edges
.iter()
.chain(dataflow_edges.iter())
.filter_map(|edge| edge.id.clone())
.collect::<Vec<_>>();
let links = state
.graphs
.links
.iter()
.filter(|link| {
node_id_set.contains(link.architecture_node_id.as_str())
|| node_id_set.contains(link.dataflow_node_id.as_str())
})
.cloned()
.collect::<Vec<_>>();
(
ThreadEpiphanyContextStateStatus::Ready,
Some(state.revision),
ThreadEpiphanyGraphContext {
architecture_nodes,
architecture_edges,
dataflow_nodes,
dataflow_edges,
links,
},
state.graph_frontier.clone(),
state.graph_checkpoint.clone(),
ThreadEpiphanyGraphQueryMatched {
node_ids: matched_node_ids,
edge_ids: matched_edge_ids,
paths: matched_paths,
symbols: matched_symbols,
edge_kinds: matched_edge_kinds,
},
ThreadEpiphanyGraphQueryMissing {
node_ids: node_ids
.iter()
.filter(|id| !found_node_ids.contains(*id))
.cloned()
.collect(),
edge_ids: edge_ids
.iter()
.filter(|id| !found_edge_ids.contains(*id))
.cloned()
.collect(),
},
)
}
fn collect_graph_query_path_matches(
state: &EpiphanyThreadState,
query: &ThreadEpiphanyGraphQuery,
node_ids: &mut Vec<String>,
edge_ids: &mut Vec<String>,
matched_paths: &mut Vec<PathBuf>,
matched_symbols: &mut Vec<String>,
) {
if query.paths.is_empty() && query.symbols.is_empty() {
return;
}
for node in state
.graphs
.architecture
.nodes
.iter()
.chain(state.graphs.dataflow.nodes.iter())
{
if code_refs_match_query(&node.code_refs, query, matched_paths, matched_symbols) {
extend_unique_strings(node_ids, [node.id.clone()]);
}
}
for edge in state
.graphs
.architecture
.edges
.iter()
.chain(state.graphs.dataflow.edges.iter())
{
if code_refs_match_query(&edge.code_refs, query, matched_paths, matched_symbols) {
if let Some(id) = edge.id.clone() {
extend_unique_strings(edge_ids, [id]);
}
extend_unique_strings(node_ids, [edge.source_id.clone(), edge.target_id.clone()]);
}
}
for link in &state.graphs.links {
if code_refs_match_query(&link.code_refs, query, matched_paths, matched_symbols) {
extend_unique_strings(
node_ids,
[
link.dataflow_node_id.clone(),
link.architecture_node_id.clone(),
],
);
}
}
}
fn collect_graph_query_edge_kind_matches(
state: &EpiphanyThreadState,
query: &ThreadEpiphanyGraphQuery,
node_ids: &mut Vec<String>,
edge_ids: &mut Vec<String>,
matched_edge_kinds: &mut Vec<String>,
) {
if query.edge_kinds.is_empty() {
return;
}
let wanted: HashSet<&str> = query.edge_kinds.iter().map(String::as_str).collect();
for edge in state
.graphs
.architecture
.edges
.iter()
.chain(state.graphs.dataflow.edges.iter())
{
if wanted.contains(edge.kind.as_str()) {
extend_unique_strings(matched_edge_kinds, [edge.kind.clone()]);
if let Some(id) = edge.id.clone() {
extend_unique_strings(edge_ids, [id]);
}
extend_unique_strings(node_ids, [edge.source_id.clone(), edge.target_id.clone()]);
}
}
}
fn expand_graph_query_neighbors(
state: &EpiphanyThreadState,
query: &ThreadEpiphanyGraphQuery,
node_ids: &mut Vec<String>,
edge_ids: &mut Vec<String>,
) {
let direction = query
.direction
.unwrap_or(ThreadEpiphanyGraphQueryDirection::Both);
let depth = query.depth.unwrap_or(1).clamp(1, 3);
for _ in 0..depth {
let current: HashSet<String> = node_ids.iter().cloned().collect();
let before = node_ids.len();
for edge in state
.graphs
.architecture
.edges
.iter()
.chain(state.graphs.dataflow.edges.iter())
{
let source_selected = current.contains(&edge.source_id);
let target_selected = current.contains(&edge.target_id);
let include_outgoing = matches!(
direction,
ThreadEpiphanyGraphQueryDirection::Outgoing
| ThreadEpiphanyGraphQueryDirection::Both
) && source_selected;
let include_incoming = matches!(
direction,
ThreadEpiphanyGraphQueryDirection::Incoming
| ThreadEpiphanyGraphQueryDirection::Both
) && target_selected;
if include_outgoing || include_incoming {
if let Some(id) = edge.id.clone() {
extend_unique_strings(edge_ids, [id]);
}
}
if include_outgoing {
extend_unique_strings(node_ids, [edge.target_id.clone()]);
}
if include_incoming {
extend_unique_strings(node_ids, [edge.source_id.clone()]);
}
}
if node_ids.len() == before {
break;
}
}
}
fn expand_graph_query_links(state: &EpiphanyThreadState, node_ids: &mut Vec<String>) {
let current: HashSet<&str> = node_ids.iter().map(String::as_str).collect();
let mut linked = Vec::new();
for link in &state.graphs.links {
if current.contains(link.dataflow_node_id.as_str()) {
linked.push(link.architecture_node_id.clone());
}
if current.contains(link.architecture_node_id.as_str()) {
linked.push(link.dataflow_node_id.clone());
}
}
extend_unique_strings(node_ids, linked);
}
fn code_refs_match_query(
code_refs: &[codex_protocol::protocol::EpiphanyCodeRef],
query: &ThreadEpiphanyGraphQuery,
matched_paths: &mut Vec<PathBuf>,
matched_symbols: &mut Vec<String>,
) -> bool {
code_refs.iter().any(|code_ref| {
let path_matches = query.paths.is_empty()
|| query.paths.iter().any(|path| {
code_ref.path == *path
|| code_ref.path.ends_with(path)
|| path.ends_with(code_ref.path.as_path())
});
let symbol_matches = query.symbols.is_empty()
|| code_ref
.symbol
.as_deref()
.is_some_and(|symbol| query.symbols.iter().any(|wanted| wanted == symbol));
if path_matches && symbol_matches {
if !query.paths.is_empty() {
extend_unique_paths(matched_paths, [code_ref.path.clone()]);
}
if let Some(symbol) = code_ref.symbol.clone()
&& !query.symbols.is_empty()
{
extend_unique_strings(matched_symbols, [symbol]);
}
true
} else {
false
}
})
}
fn extend_unique_paths(target: &mut Vec<PathBuf>, values: impl IntoIterator<Item = PathBuf>) {
for value in values {
if !target.iter().any(|existing| existing == &value) {
target.push(value);
}
}
}
fn select_epiphany_graph_edges(
edges: &[codex_protocol::protocol::EpiphanyGraphEdge],
graph_node_id_set: &HashSet<&str>,
@ -17575,6 +17977,7 @@ mod tests {
ThreadEpiphanySceneAction::Retrieve,
ThreadEpiphanySceneAction::Distill,
ThreadEpiphanySceneAction::Context,
ThreadEpiphanySceneAction::GraphQuery,
ThreadEpiphanySceneAction::Jobs,
ThreadEpiphanySceneAction::Roles,
ThreadEpiphanySceneAction::Coordinator,
@ -19147,6 +19550,115 @@ mod tests {
assert_eq!(missing.observation_ids, vec!["obs-1".to_string()]);
}
#[test]
fn map_epiphany_graph_query_returns_frontier_neighborhood() {
let state = codex_protocol::protocol::EpiphanyThreadState {
revision: 3,
graphs: codex_protocol::protocol::EpiphanyGraphs {
architecture: codex_protocol::protocol::EpiphanyGraph {
nodes: vec![
codex_protocol::protocol::EpiphanyGraphNode {
id: "operator-console".to_string(),
title: "Operator console".to_string(),
purpose: "Expose bounded controls.".to_string(),
code_refs: vec![codex_protocol::protocol::EpiphanyCodeRef {
path: PathBuf::from("apps/epiphany-gui/src/App.tsx"),
symbol: Some("App".to_string()),
..Default::default()
}],
..Default::default()
},
codex_protocol::protocol::EpiphanyGraphNode {
id: "action-bridge".to_string(),
title: "Action bridge".to_string(),
purpose: "Call app-server APIs.".to_string(),
..Default::default()
},
],
edges: vec![codex_protocol::protocol::EpiphanyGraphEdge {
id: Some("edge-console-bridge".to_string()),
source_id: "operator-console".to_string(),
target_id: "action-bridge".to_string(),
kind: "invokes".to_string(),
..Default::default()
}],
},
dataflow: codex_protocol::protocol::EpiphanyGraph {
nodes: vec![codex_protocol::protocol::EpiphanyGraphNode {
id: "operator-action-flow".to_string(),
title: "Operator action flow".to_string(),
purpose: "Button to Tauri to Python to app-server.".to_string(),
..Default::default()
}],
edges: Vec::new(),
},
links: vec![codex_protocol::protocol::EpiphanyGraphLink {
dataflow_node_id: "operator-action-flow".to_string(),
architecture_node_id: "operator-console".to_string(),
relationship: Some("starts-at".to_string()),
code_refs: Vec::new(),
}],
},
graph_frontier: Some(codex_protocol::protocol::EpiphanyGraphFrontier {
active_node_ids: vec!["operator-console".to_string()],
..Default::default()
}),
..Default::default()
};
let query = ThreadEpiphanyGraphQuery {
kind: ThreadEpiphanyGraphQueryKind::FrontierNeighborhood,
node_ids: Vec::new(),
edge_ids: Vec::new(),
paths: Vec::new(),
symbols: Vec::new(),
edge_kinds: Vec::new(),
direction: Some(ThreadEpiphanyGraphQueryDirection::Outgoing),
depth: Some(1),
include_links: None,
};
let (state_status, state_revision, graph, frontier, _checkpoint, matched, missing) =
map_epiphany_graph_query(Some(&state), &query);
assert_eq!(state_status, ThreadEpiphanyContextStateStatus::Ready);
assert_eq!(state_revision, Some(3));
assert_eq!(
graph
.architecture_nodes
.iter()
.map(|node| node.id.as_str())
.collect::<Vec<_>>(),
vec!["operator-console", "action-bridge"]
);
assert_eq!(
graph
.dataflow_nodes
.iter()
.map(|node| node.id.as_str())
.collect::<Vec<_>>(),
vec!["operator-action-flow"]
);
assert_eq!(
graph
.architecture_edges
.iter()
.filter_map(|edge| edge.id.as_deref())
.collect::<Vec<_>>(),
vec!["edge-console-bridge"]
);
assert_eq!(graph.links.len(), 1);
assert_eq!(
frontier
.as_ref()
.map(|frontier| frontier.active_node_ids.as_slice()),
Some(&["operator-console".to_string()][..])
);
assert_eq!(matched.edge_ids, vec!["edge-console-bridge".to_string()]);
assert!(missing.node_ids.is_empty());
assert!(missing.edge_ids.is_empty());
}
#[test]
fn map_epiphany_jobs_reflects_current_progress_without_scheduling_work() {
let state = codex_protocol::protocol::EpiphanyThreadState {