From aab3ae3334de3fbc2e705d755b8a5a15b655da99 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 26 Apr 2026 03:00:06 +0100 Subject: [PATCH] Add Phase 6 context shard reflection --- notes/epiphany-core-harness-surfaces.md | 26 +- notes/epiphany-current-algorithmic-map.md | 132 +++++- notes/epiphany-fork-implementation-plan.md | 18 +- notes/fresh-workspace-handoff.md | 19 +- state/evidence.jsonl | 1 + state/map.yaml | 16 +- state/scratch.md | 16 +- tools/epiphany_phase6_context_smoke.py | 301 ++++++++++++ tools/epiphany_phase6_scene_smoke.py | 4 +- .../codex_app_server_protocol.schemas.json | 122 +++++ .../codex_app_server_protocol.v2.schemas.json | 122 +++++ .../typescript/v2/ThreadEpiphanyContext.ts | 10 + .../v2/ThreadEpiphanyContextMissing.ts | 5 + .../v2/ThreadEpiphanyContextSource.ts | 5 + .../v2/ThreadEpiphanyContextStateStatus.ts | 5 + .../v2/ThreadEpiphanyGraphContext.ts | 8 + .../v2/ThreadEpiphanySceneAction.ts | 2 +- .../schema/typescript/v2/index.ts | 5 + .../src/protocol/common.rs | 137 ++++++ .../app-server-protocol/src/protocol/v2.rs | 105 ++++ .../app-server/src/codex_message_processor.rs | 447 ++++++++++++++++++ 21 files changed, 1459 insertions(+), 47 deletions(-) create mode 100644 tools/epiphany_phase6_context_smoke.py create mode 100644 vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyContext.ts create mode 100644 vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyContextMissing.ts create mode 100644 vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyContextSource.ts create mode 100644 vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyContextStateStatus.ts create mode 100644 vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyGraphContext.ts diff --git a/notes/epiphany-core-harness-surfaces.md b/notes/epiphany-core-harness-surfaces.md index 1918ea1d..2e43d0ad 100644 --- a/notes/epiphany-core-harness-surfaces.md +++ b/notes/epiphany-core-harness-surfaces.md @@ -48,6 +48,7 @@ The rule is: | `thread/epiphany/stateUpdated` | notification | landed | Emits updated projected state, source, revision, and changed fields after successful update/promote writes. | | `thread/epiphany/scene` | read-only reflection | landed, live-smoked | Compact client scene derived from authoritative Epiphany state. | | `thread/epiphany/jobs` | read-only reflection | landed, live-smoked | Derived indexing, remap, verification, and specialist-progress slots from typed state and retrieval summaries. | +| `thread/epiphany/context` | read-only reflection | landed, live-smoked | Targeted state shard for graph nodes/edges, active frontier, checkpoint, observations, and evidence. | ## Write Authority @@ -66,14 +67,15 @@ The following must stay read-only: - `thread/epiphany/propose` - `thread/epiphany/scene` - `thread/epiphany/jobs` +- `thread/epiphany/context` `thread/epiphany/index` is a write to the retrieval catalog. It is not a license to mutate map/evidence/churn state as a side effect. ## Reflection Authority -Scene, jobs, and GUI surfaces may compress state for humans and clients. They -may not invent canonical understanding. +Scene, jobs, context, and GUI surfaces may compress state for humans and +clients. They may not invent canonical understanding. Reflection surfaces should: @@ -122,7 +124,7 @@ These are not landed yet: - live typed job/progress state for running indexing, remap, verification, and specialist work - `thread/epiphany/jobsUpdated` or equivalent progress notifications -- evidence-range and graph-shard targeted reads +- richer evidence-range and graph-shard inspection beyond the landed context shard - watcher-driven graph/retrieval/invariant invalidation - automatic tool-output observation promotion - typed turn intent before broad mutation @@ -167,6 +169,24 @@ Minimum useful fields: The landed reflection surface keeps this read-only. Future live job state must keep the same rule: do not make job state a hidden second planner. +## Context Shard Surface Direction + +The first targeted context read is landed as `thread/epiphany/context`. + +It selects from existing typed state only: + +- architecture/dataflow nodes by id plus active frontier nodes by default +- graph edges by id plus incident edges for selected nodes +- graph links touching selected nodes +- active frontier and current graph checkpoint +- observations by id +- evidence by id plus evidence linked from selected observations by default +- missing requested or active-frontier ids, so clients do not have to guess whether a shard was empty or stale + +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. + ## Compaction And CRRC Compact-Rehydrate-Reorient-Continue is an architecture primitive, but automatic diff --git a/notes/epiphany-current-algorithmic-map.md b/notes/epiphany-current-algorithmic-map.md index cef0d513..b36794a2 100644 --- a/notes/epiphany-current-algorithmic-map.md +++ b/notes/epiphany-current-algorithmic-map.md @@ -10,21 +10,22 @@ 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 thirteen live paths: +The spine exists in fourteen live paths: - protocol state shape: `EpiphanyThreadState`, `RolloutItem::EpiphanyState`, retrieval summaries, and prompt tags live in [protocol.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/protocol/src/protocol.rs:103). - in-memory session state: `SessionState` stores an optional `EpiphanyThreadState`, and `Session` exposes thin async accessors over it in [state/session.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/state/session.rs:36) and [session/mod.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/session/mod.rs:1240). - prompt injection: `build_initial_context` reads the optional state and adds a bounded `` developer fragment in [session/mod.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/session/mod.rs:2434). - per-turn persistence: normal turn setup persists one `RolloutItem::EpiphanyState` after the `TurnContext` when state exists in [session/mod.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/session/mod.rs:2677). -- client read hydration: app-server thread views attach live or reconstructed `thread.epiphanyState` in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4599). -- explicit distillation proposals: app-server routes read-only `thread/epiphany/distill` through a loaded-thread handler into `epiphany-core` so one explicit observation can become a patch candidate without mutating state; tool/command/shell/model sources now get typed evidence kinds and bounded salient-output summaries instead of dumping raw output into durable state in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4173) and [distillation.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/distillation.rs:28). -- explicit map/churn proposals: app-server routes read-only `thread/epiphany/propose` through a loaded-thread handler into `epiphany-core` so verified observations with code refs and accepting recent evidence can be selected explicitly or auto-selected as a bounded path cluster, prioritize the strongest selected observation, focus or extend architecture graph nodes, rescue unanchored graph nodes through strict semantic overlap, carry linked dataflow nodes and incident graph edges into the frontier, update churn candidates with match-kind-aware map-delta judgment and pressure, and still avoid mutation until promotion in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4244) and [proposal.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/proposal.rs:115). -- explicit promotion gates: app-server routes `thread/epiphany/promote` through a loaded-thread handler into `epiphany-core` policy evaluation, rejects failed verifier evidence without mutation, applies accepted candidates through the same durable update path, and now treats medium/high/expanded/broadening/semantic churn deltas as needing explicit rationale plus stronger verifier evidence whose kind is token-matched rather than substring-matched in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4316) and [promotion.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/promotion.rs:33). -- explicit state updates: app-server routes `thread/epiphany/update` through a loaded `CodexThread` update method that rejects malformed appended observation/evidence graph records and structurally invalid replacement fields, mutates live `SessionState` only after validation, bumps the revision, and persists an immediate `RolloutItem::EpiphanyState` snapshot in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4436), [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:374), [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:552), and [promotion.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/promotion.rs:44). -- live state notifications and write responses: app-server declares experimental `thread/epiphany/stateUpdated` and emits it with the updated typed state, typed `source`, event-level `revision`, and typed `changedFields` after successful direct updates and accepted promotions, but not after rejected promotions; direct `thread/epiphany/update` and accepted `thread/epiphany/promote` responses expose the same revision/change metadata, accepted promotions always report `Evidence` because verifier evidence is appended even when the patch evidence list is empty, and successful write response/notification states now use the same client-visible live projection as `thread/read`, including retrieval-summary backfill when durable state has no persisted retrieval metadata, in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:1097), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:3992), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4058), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4069), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4381), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4415), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4470), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4502), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10430), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10440), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10526), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10569). -- scene projection: app-server declares experimental read-only `thread/epiphany/scene`, reads the same live/stored Epiphany state view used by `thread/read`, and derives a compact client scene with objective, active subgoal, invariant status counts, graph focus/counts, retrieval status, observation/evidence summaries, churn, live/stored source, and available control-plane actions without mutating or persisting state in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:340), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:3921), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4079), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10573). -- job/progress reflection: app-server declares experimental read-only `thread/epiphany/jobs`, reads the same thread view as scene, optionally fills retrieval state for loaded threads with no Epiphany state, and derives four non-authoritative progress slots for retrieval indexing, graph remap, invariant verification, and future specialist work without scheduling, notifying, mutating, or persisting jobs in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:344), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4100), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4114), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10734). -- retrieval/indexing: app-server routes `thread/epiphany/retrieve` and `thread/epiphany/index` through loaded `CodexThread` host methods into `epiphany-core` in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4046), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4122), [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:438), and [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:456). +- client read hydration: app-server thread views attach live or reconstructed `thread.epiphanyState` in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4706) and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4822). +- explicit distillation proposals: app-server routes read-only `thread/epiphany/distill` through a loaded-thread handler into `epiphany-core` so one explicit observation can become a patch candidate without mutating state; tool/command/shell/model sources now get typed evidence kinds and bounded salient-output summaries instead of dumping raw output into durable state in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4358) and [distillation.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/distillation.rs:28). +- explicit map/churn proposals: app-server routes read-only `thread/epiphany/propose` through a loaded-thread handler into `epiphany-core` so verified observations with code refs and accepting recent evidence can be selected explicitly or auto-selected as a bounded path cluster, prioritize the strongest selected observation, focus or extend architecture graph nodes, rescue unanchored graph nodes through strict semantic overlap, carry linked dataflow nodes and incident graph edges into the frontier, update churn candidates with match-kind-aware map-delta judgment and pressure, and still avoid mutation until promotion in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4429) and [proposal.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/proposal.rs:115). +- explicit promotion gates: app-server routes `thread/epiphany/promote` through a loaded-thread handler into `epiphany-core` policy evaluation, rejects failed verifier evidence without mutation, applies accepted candidates through the same durable update path, and now treats medium/high/expanded/broadening/semantic churn deltas as needing explicit rationale plus stronger verifier evidence whose kind is token-matched rather than substring-matched in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4501) and [promotion.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/promotion.rs:33). +- explicit state updates: app-server routes `thread/epiphany/update` through a loaded `CodexThread` update method that rejects malformed appended observation/evidence graph records and structurally invalid replacement fields, mutates live `SessionState` only after validation, bumps the revision, and persists an immediate `RolloutItem::EpiphanyState` snapshot in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4620), [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:376), [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:552), and [promotion.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/promotion.rs:44). +- live state notifications and write responses: app-server declares experimental `thread/epiphany/stateUpdated` and emits it with the updated typed state, typed `source`, event-level `revision`, and typed `changedFields` after successful direct updates and accepted promotions, but not after rejected promotions; direct `thread/epiphany/update` and accepted `thread/epiphany/promote` responses expose the same revision/change metadata, accepted promotions always report `Evidence` because verifier evidence is appended even when the patch evidence list is empty, and successful write response/notification states now use the same client-visible live projection as `thread/read`, including retrieval-summary backfill when durable state has no persisted retrieval metadata, in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:1113), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4354), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4419), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4428), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4555), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4608), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4652), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4693), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11327), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11370). +- scene projection: app-server declares experimental read-only `thread/epiphany/scene`, reads the same live/stored Epiphany state view used by `thread/read`, and derives a compact client scene with objective, active subgoal, invariant status counts, graph focus/counts, retrieval status, observation/evidence summaries, churn, live/stored source, and available control-plane actions without mutating or persisting state in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:340), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:3924), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4090), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10629). +- job/progress reflection: app-server declares experimental read-only `thread/epiphany/jobs`, reads the same thread view as scene, optionally fills retrieval state for loaded threads with no Epiphany state, and derives four non-authoritative progress slots for retrieval indexing, graph remap, invariant verification, and future specialist work without scheduling, notifying, mutating, or persisting jobs in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:345), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4104), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4125), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10992). +- targeted context reflection: app-server declares experimental read-only `thread/epiphany/context`, reads the same live/stored Epiphany state view used by scene/jobs, and derives a bounded state shard containing selected graph nodes/edges, links, active frontier, checkpoint, observations, direct evidence, linked evidence, and missing requested ids without retrieval, proposal, promotion, notification, mutation, or persistence in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:350), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4182), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4186), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10791). +- retrieval/indexing: app-server routes `thread/epiphany/retrieve` and `thread/epiphany/index` through loaded `CodexThread` host methods into `epiphany-core` in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4231), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4149), [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:467), and [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:449). The thick Epiphany-owned implementation is mostly outside vendored Codex now: @@ -63,6 +64,8 @@ flowchart TD QS --> Q N --> QJ["thread/epiphany/jobs derives progress slots"] QJ --> Q + N --> QC["thread/epiphany/context derives targeted state shard"] + QC --> Q A --> R["thread/epiphany/retrieve requires loaded thread"] A --> S["thread/epiphany/index requires loaded thread"] A --> AA["thread/epiphany/distill requires loaded thread"] @@ -103,6 +106,7 @@ What this means in plain English: - The app-server can expose it as typed thread data. - The app-server can also expose a compact read-only scene derived from that typed state for future clients. - 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 targeted read-only context shard for clients that need graph/evidence detail without pulling the full typed state. - 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. - A loaded thread can now accept explicit typed state patches and persist them immediately. @@ -114,13 +118,13 @@ What this means in plain English: ## Perfect Machine Audit Result -Source audit on 2026-04-25 re-read each flow below against the cited code instead of just checking that links resolve. +Source audit on 2026-04-26 re-read the changed Phase 6 reflection paths against the cited code instead of just checking that links resolve. The current landed machine is still coherent. The core shape is not ornamental: one protocol state object flows through session state, prompt rendering, rollout persistence, thread hydration, explicit retrieval/indexing, read-only distillation/proposal, verifier-backed promotion, the single durable update writer, and now a live state notification that reflects completed writes. The useful simplification pressure is already present in the boundaries: retrieval reads, indexing writes only the semantic catalog, distillation/proposal draft patches, promotion gates, update persists, and notification broadcasts only after the write is done. Nothing in the current typed spine obviously deserves deletion right now. The parts that would turn this into Jenga are still correctly listed as non-flows: automatic tool-output ingestion, watcher invalidation, specialist scheduling, broad event streaming, and an automatic Compact-Rehydrate-Reorient-Continue coordinator. Thin GUI/client reflection is no longer forbidden as a category, provided it reflects and steers typed state rather than inventing a second source of truth. -The next Perfect Machine move is not adding another writer, not re-hardening Phase 5, and no longer the first scene or job reflection. Those surfaces now let clients inspect the machine Epiphany already built. The next outward work should add a missing observation source only when there is a concrete consumer: targeted graph/evidence reads, watcher/freshness inputs, or live progress notifications once real long-running job owners exist. +The next Perfect Machine move is not adding another writer, not re-hardening Phase 5, and no longer the first scene, job, or targeted context reflection. Those surfaces now let clients inspect the machine Epiphany already built at summary, progress, and bounded-detail levels. The next outward work should add a missing observation source only when there is a concrete consumer: watcher/freshness inputs, or live progress notifications once real long-running job owners exist. ## Natural Language Spine @@ -137,6 +141,7 @@ This is the same flow in the language Epiphany is supposed to make the model car | Thread read | Clients can ask for the current Epiphany model as typed data instead of scraping prompt text or transcript debris. | The dashboard reads the blueprint file directly, not a photo of the workbench. | | Scene projection | Clients can ask for a compact reflection of the typed model: active goal, graph focus, retrieval status, evidence counts, churn, and available control actions. It is derived from state, not a second state store. | A foreman-facing wall board copied from the blueprint ledger; useful at a glance, legally not the blueprint. | | Job projection | Clients can ask which Epiphany work slots are idle, needed, blocked, running, or unavailable for indexing, remap, verification, and future specialists. The slots are derived from state and retrieval summaries; they do not start work. | A shift board listing which crews would be needed if work begins, not a foreman secretly hiring people in the back room. | +| Context projection | Clients can ask for one bounded slice of the typed model: selected graph nodes/edges, active frontier, checkpoint, observations, direct evidence, linked evidence, and missing ids. It is derived from state and does not propose or promote anything. | A librarian opens the exact ledger pages you named, adds the active bookmark by default, and says which requested tabs were not in the book. | | Retrieval | A loaded thread can ask a structured question of the repo through one typed retrieval surface. | A librarian brings back marked pages instead of making the agent rummage through every shelf. | | Indexing | Persistent semantic memory is built only through an explicit indexing path. | The librarian updates the card catalog only when asked, not while pretending to answer a question. | | Distillation | One explicit observation can be normalized into a typed observation/evidence patch, but not promoted automatically. | The clerk drafts a ledger entry in pencil before anyone is allowed to ink it. | @@ -954,7 +959,7 @@ The app-server handler: 6. summarizes retrieval status, semantic availability, index revision, shard count, dirty-path count, and indexed file/chunk counts. 7. includes bounded latest observation/evidence summaries in newest-first order, matching the durable state's recent-record ordering. 8. reflects churn pressure/warning fields. -9. reports available actions only when the thread is loaded, including the read-only `jobs` door and adding `propose`/`promote` only when state exists. +9. reports available actions only when the thread is loaded, including the read-only `context` and `jobs` doors and adding `propose`/`promote` only when state exists. ### Output @@ -1042,6 +1047,98 @@ The response is a typed progress board derived from the current Epiphany machine Job projection is read-only. It does not start indexing, remap graphs, verify invariants, schedule specialists, mutate `SessionState`, append rollout items, or emit progress notifications. It is a board on the wall, not a payroll department with a fake mustache. +## Flow 15: Targeted Context Shard Control Flow + +### Input + +A client sends experimental `thread/epiphany/context` with: + +- `threadId` +- optional `graphNodeIds` +- optional `graphEdgeIds` +- optional `observationIds` +- optional `evidenceIds` +- optional `includeActiveFrontier` +- optional `includeLinkedEvidence` + +### Plain-language role + +Context projection is the third Phase 6 reflection boundary. It gives clients a bounded way to ask "show me the exact Epiphany state slice around this graph/evidence target" without hauling the full `EpiphanyThreadState`, running retrieval, drafting proposals, or letting the GUI manufacture canonical understanding. + +The useful distinction from scene and jobs is resolution. Scene is the wall board. Jobs is the shift board. Context is the opened binder page: selected graph nodes and edges, links, active frontier, checkpoint, observations, evidence, and any requested ids that were not found. + +Protocol shape: + +- `threadId` +- `graphNodeIds` +- `graphEdgeIds` +- `observationIds` +- `evidenceIds` +- optional `includeActiveFrontier`, defaulting true in the mapper +- optional `includeLinkedEvidence`, defaulting true in the mapper + +Response shape: + +- `threadId` +- `source` +- `stateStatus` +- optional `stateRevision` +- `context` + - `graph` + - `architectureNodes` + - `architectureEdges` + - `dataflowNodes` + - `dataflowEdges` + - `links` + - optional `frontier` + - optional `checkpoint` + - `observations` + - `evidence` +- `missing` + - `graphNodeIds` + - `graphEdgeIds` + - `observationIds` + - `evidenceIds` + +Code refs: + +- [app-server-protocol/common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:350) +- [app-server-protocol/v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4182) +- [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4186) +- [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10791) + +### Mechanism + +The app-server handler: + +1. parses the thread id. +2. checks whether the thread is loaded so the response can report live/stored source. +3. calls `read_thread_view(..., include_turns: false)` to reuse the live/stored Epiphany projection path. +4. passes the optional `thread.epiphany_state` and selectors into `map_epiphany_context`. +5. returns the derived context shard and missing-id lists. + +`map_epiphany_context`: + +1. returns missing state with an empty context and echoes requested ids when no Epiphany state exists. +2. defaults `includeActiveFrontier` to true, so a client can ask for the current working slice with only a thread id. +3. defaults `includeLinkedEvidence` to true, so selected observations bring their evidence anchors. +4. de-duplicates requested ids while preserving request/frontier order. +5. selects architecture and dataflow nodes by id. +6. selects graph edges by explicit edge id and incident edges touching selected nodes. +7. includes graph links touching selected architecture or dataflow nodes. +8. includes active frontier and graph checkpoint when available. +9. selects observations by id. +10. selects directly requested evidence plus evidence ids linked from selected observations. +11. reports any requested or active-frontier graph ids, observation ids, or evidence ids not found in the authoritative typed state. + +### Output + +The response is a bounded state shard for a client or model-facing coordinator that already knows which part of the Epiphany model it wants to inspect. The Phase 6 context smoke proved missing-state context reports live source without inventing a revision or graph records, ready context preserves revision identity, returns the active graph node/edge and linked/direct evidence, emits no `thread/epiphany/stateUpdated` notification, and leaves final thread state revision unchanged. + +### Invariant + +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. + ## Current Non-Flows These are deliberately not shipped yet: @@ -1053,7 +1150,7 @@ These are deliberately not shipped yet: - no code-intelligence graph. - no specialist-agent scheduler. - no live long-running job execution or `thread/epiphany/jobsUpdated` progress notification. -- no GUI implementation beyond the read-only scene and jobs projection surfaces. +- no GUI implementation beyond the read-only scene, jobs, and context projection surfaces. - no durable retrieval-summary write from `thread/epiphany/retrieve`. The current system is therefore not "the model maintains a map automatically." More precisely: @@ -1065,6 +1162,7 @@ stored Epiphany state -> visible to clients -> reflected as a compact read-only scene for clients -> reflected as read-only job/progress slots for clients +-> reflected as targeted read-only graph/evidence context shards for clients -> retrieval/indexing can inform future work -> explicit distillation can draft observation/evidence patches -> explicit proposal can draft map/churn patches from verified observations @@ -1073,7 +1171,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, and no longer the first job/progress reflection. Those exist. Proposal now has the first bits of map memory, focus, and selection hygiene: it can auto-select a bounded evidence-backed observation cluster when ids are omitted, requires accepting recent evidence behind selected observations, prioritizes stronger selected observations for proposal wording, reuses existing architecture nodes by concrete code-ref/path/id checks before creating new path nodes, can rescue unanchored graph nodes through strict unique semantic overlap, follows graph links and incident edges into the frontier, and reports match-kind-aware churn pressure from the actual proposal shape. Distillation now also has the first source-output-aware teeth for tool/model summaries. Promotion now notices risky deltas instead of just checking the shape of the envelope, treats graph expansion as risky even if the patch tries to mumble "low pressure" while dragging a new wall into the blueprint, and token-matches strong verifier kinds so `contest` cannot sneak past as `test`. Successful update/promote writes now emit `thread/epiphany/stateUpdated` with `source: "update"` or `source: "promote"`, event-level revision, and typed changed fields so clients no longer need to poll blindly, infer the cause from timing, or diff the whole state to know which top-level sections moved. The scene and jobs surfaces give clients compact read-only views without making the GUI/client authoritative, and live app-server smoke now guards those boundaries. The remaining missing organs are watcher/freshness inputs, targeted graph/evidence reads, live job progress notifications once real job owners exist, and eventually role-scoped specialist ownership without silently auto-promoting anything. +The current remaining missing organ is not the red pen, not the first chewing motion, not the first state-change bell, not the first client-shaped scene, not the first job/progress reflection, and no longer the first targeted graph/evidence context read. Those exist. Proposal now has the first bits of map memory, focus, and selection hygiene: it can auto-select a bounded evidence-backed observation cluster when ids are omitted, requires accepting recent evidence behind selected observations, prioritizes stronger selected observations for proposal wording, reuses existing architecture nodes by concrete code-ref/path/id checks before creating new path nodes, can rescue unanchored graph nodes through strict unique semantic overlap, follows graph links and incident edges into the frontier, and reports match-kind-aware churn pressure from the actual proposal shape. Distillation now also has the first source-output-aware teeth for tool/model summaries. Promotion now notices risky deltas instead of just checking the shape of the envelope, treats graph expansion as risky even if the patch tries to mumble "low pressure" while dragging a new wall into the blueprint, and token-matches strong verifier kinds so `contest` cannot sneak past as `test`. Successful update/promote writes now emit `thread/epiphany/stateUpdated` with `source: "update"` or `source: "promote"`, event-level revision, and typed changed fields so clients no longer need to poll blindly, infer the cause from timing, or diff the whole state to know which top-level sections moved. The scene, jobs, and context surfaces give clients compact or bounded read-only views without making the GUI/client authoritative, and live app-server smoke now guards those boundaries. The remaining missing organs are watcher/freshness inputs, live job progress notifications once real job owners exist, and eventually role-scoped specialist ownership without silently auto-promoting anything. ```text model/tool observations @@ -1087,7 +1185,7 @@ 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, 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, 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 ingest fresh tool/model output into richer map deltas automatically, invalidate stale graph regions, execute long-running Epiphany jobs as tracked background work, emit job progress notifications, or coordinate specialist ownership. Teeth are installed; chewing is supervised; the first bell rings; the first wall board exists; the first shift board exists; the next slices are about senses, not jawbones. +In natural language: current Epiphany can preserve a map, show a map, project a compact scene from the map, project derived job/progress slots from the map and retrieval summary, project a targeted graph/evidence context shard from the map, 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 ingest fresh tool/model output into richer map deltas automatically, invalidate stale graph regions, execute long-running Epiphany jobs as tracked background work, emit job progress notifications, or coordinate specialist ownership. Teeth are installed; chewing is supervised; the first bell rings; the first wall board exists; the first shift board exists; the first binder lens exists; the next slices are about senses, not jawbones. ## Verification Hooks @@ -1107,6 +1205,7 @@ Current tests cover the landed flows at useful seams: - reusable app-server smoke for the richer Phase 5 chain in `tools/epiphany_phase5_smoke.py`, including update/promote response metadata, `thread/epiphany/stateUpdated` notifications with source, revision, changed-field checks, retrieval-summary presence in successful write response/notification states, the verifier-only promotion edge where `Evidence` changes because verifier evidence is appended, invalid direct observation/evidence and replacement update rejection without mutation or notification, and explicit zero-notification assertions for rejected promotions. - live app-server smoke for the first Phase 6 scene boundary in `tools/epiphany_phase6_scene_smoke.py`. - live app-server smoke for the first Phase 6 jobs boundary in `tools/epiphany_phase6_jobs_smoke.py`. +- live app-server smoke for the first Phase 6 context boundary in `tools/epiphany_phase6_context_smoke.py`. Useful commands: @@ -1118,4 +1217,5 @@ $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_phase5_smoke.py' & 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_scene_smoke.py' & 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_jobs_smoke.py' +& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_context_smoke.py' ``` diff --git a/notes/epiphany-fork-implementation-plan.md b/notes/epiphany-fork-implementation-plan.md index a2f6cee2..f6ced204 100644 --- a/notes/epiphany-fork-implementation-plan.md +++ b/notes/epiphany-fork-implementation-plan.md @@ -59,6 +59,8 @@ The landed machine now has: - live Phase 6 scene app-server smoke coverage in `tools/epiphany_phase6_scene_smoke.py` - read-only Phase 6 job/progress reflection through `thread/epiphany/jobs` - live Phase 6 jobs app-server smoke coverage in `tools/epiphany_phase6_jobs_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` The current phase is Phase 6: reflection boundary and observable harness state. @@ -73,6 +75,7 @@ These boundaries are more important than the individual method names: - `thread/epiphany/index` may update the semantic retrieval catalog, but it is not a hidden Epiphany-state writer. - `thread/epiphany/scene` is a client reflection, not a second source of truth. - `thread/epiphany/jobs` is a derived reflection, not a scheduler or durable job store. +- `thread/epiphany/context` is a targeted reflection, not a state writer or hidden proposal engine. - The GUI may render and steer typed state, but it must not manufacture canonical understanding. - The app-server remains a host seam; Epiphany-owned machinery should live in `epiphany-core` where practical. - Qdrant is the preferred persistent semantic backend; BM25 remains the bootstrap/fallback/control path. @@ -119,10 +122,9 @@ Phase 6 should grow observable harness state outward from the typed spine. Useful candidates: -1. Add targeted graph/evidence shard reads when the full `EpiphanyThreadState` is too raw. -2. Add watcher/freshness inputs when stale graph or retrieval state needs earlier warning. -3. Add live job progress notifications only after there is a real long-running owner to report. -4. Add targeted scene/jobs fields only when a client or smoke exposes a real gap. +1. Add watcher/freshness inputs when stale graph or retrieval state needs earlier warning. +2. Add live job progress notifications only after there is a real long-running owner to report. +3. Add targeted scene/jobs/context fields only when a client or smoke exposes a real gap. Do not spend Phase 6 polishing Phase 5 out of anxiety. The Phase 5 smoke harness is a regression guardrail, not a ritual drum circle for summoning more tiny @@ -134,7 +136,7 @@ These remain later work: - watcher-driven semantic invalidation - automatic observation promotion from tool output -- richer evidence and graph-shard inspection surfaces +- richer evidence and graph-shard inspection beyond the landed targeted context read - role-scoped specialist-agent registry and scheduling - mutation gates that warn or block broad writes when map freshness is stale - automatic CRRC runtime coordination @@ -165,6 +167,12 @@ Before modifying jobs reflection behavior, run: & 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_jobs_smoke.py' ``` +Before modifying targeted context-shard behavior, run: + +```powershell +& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_context_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. diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 9c841c80..6fdeeba8 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -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` and `thread/epiphany/jobs`; both have live app-server smoke coverage. +- Phase 6 has read-only `thread/epiphany/scene`, `thread/epiphany/jobs`, and `thread/epiphany/context`; all three have live app-server smoke coverage. - The repo is an Epiphany fork of Codex, not a Codex preset. - `vendor/codex` is tracked directly, not a submodule. - `epiphany-core` owns the heavy Epiphany organs where practical. @@ -61,8 +61,10 @@ The current spine: - successful-write notification through `thread/epiphany/stateUpdated` - read-only compact reflection through `thread/epiphany/scene` - read-only job/progress reflection through `thread/epiphany/jobs` +- read-only targeted state-shard reflection through `thread/epiphany/context` - live scene app-server smoke through `tools/epiphany_phase6_scene_smoke.py` - live jobs app-server smoke through `tools/epiphany_phase6_jobs_smoke.py` +- live context app-server smoke through `tools/epiphany_phase6_context_smoke.py` The exact current control flow is documented in `notes/epiphany-current-algorithmic-map.md`. @@ -74,6 +76,7 @@ The exact current control flow is documented in - `thread/epiphany/propose` is read-only. - `thread/epiphany/scene` is read-only. - `thread/epiphany/jobs` is read-only. +- `thread/epiphany/context` is read-only. - Durable typed state writes go through `thread/epiphany/update` or accepted `thread/epiphany/promote`. - `thread/epiphany/index` writes the retrieval catalog, not durable Epiphany understanding. - GUI/client surfaces reflect and steer typed state; they do not become the source of truth. @@ -99,6 +102,12 @@ For jobs reflection behavior changes, run: & 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_jobs_smoke.py' ``` +For targeted context-shard behavior changes, run: + +```powershell +& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_context_smoke.py' +``` + For Codex Rust work on this Windows machine: ```powershell @@ -138,11 +147,11 @@ that change what the next agent should believe. Do not continue implementation automatically from a rehydrate-only request. When the user asks to continue, choose the next Phase 6 outward slice. Good -candidates are targeted graph/evidence shard reads, watcher/freshness inputs, -or live job progress notifications. Do not build a scheduler from vibes. +candidates are watcher/freshness inputs or live job progress notifications. Do +not build a scheduler from vibes. -Live `thread/epiphany/scene` and `thread/epiphany/jobs` smokes are now -guardrails, not the next organs. +Live `thread/epiphany/scene`, `thread/epiphany/jobs`, and +`thread/epiphany/context` smokes are now guardrails, not the next organs. ## Not Yet diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 04587ef7..c471a465 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -31,3 +31,4 @@ {"ts": "2026-04-26T00:20:30+00:00", "type": "decision", "status": "ok", "note": "Added tools/epiphany_prepare_compaction.py and local AGENTS guidance so imminent-compaction passes run a concrete persistence audit before and after state edits. The helper checks required state files, map summary/next action, scratch staleness, evidence/branch parsing, stale handoff git snapshots, AGENTS trigger wording, and dirty git state."} {"ts": "2026-04-26T00:33:31+00:00", "type": "verification", "status": "ok", "note": "Phase 6 scene live-smoked through app-server with tools/epiphany_phase6_scene_smoke.py. The smoke proves missing and ready scene shapes, live source, retrieval-summary backfill, available actions, newest-first bounded observation/evidence records, no scene-triggered stateUpdated notification, and unchanged final read revision; it also caught and fixed reversed latest-record ordering in map_epiphany_scene."} {"ts": "2026-04-26T01:08:00+00:00", "type": "verification", "status": "ok", "note": "Phase 6 jobs reflection landed as read-only thread/epiphany/jobs and live-smoked through app-server with tools/epiphany_phase6_jobs_smoke.py. The surface derives retrieval-index, graph-remap, verification, and specialist slots from thread state/retrieval summaries, preserves revision identity, emits no stateUpdated notification, and leaves live scheduling/progress notifications as future work."} +{"ts": "2026-04-26T01:58:10+00:00", "type": "verification", "status": "ok", "note": "Phase 6 targeted context reflection landed as read-only thread/epiphany/context and live-smoked through app-server with tools/epiphany_phase6_context_smoke.py. The surface returns selected graph/evidence shards plus missing ids without mutation, notification, retrieval, proposal, or promotion."} diff --git a/state/map.yaml b/state/map.yaml index cb920d2d..ecf18815 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -51,7 +51,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, and jobs. + - Preserve read-only boundaries for retrieve, distill, propose, scene, jobs, and context. - Extend Phase 6 observability only where it reflects authoritative state or retrieval summaries without becoming a scheduler. - Keep persistent project state ruthlessly distilled so rehydration strengthens the next turn instead of bloating it. @@ -59,9 +59,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 `` developer fragment. - - Clients read typed state through `Thread.epiphanyState` and compact reflections such as `thread/epiphany/scene` and `thread/epiphany/jobs`. + - Clients read typed state through `Thread.epiphanyState`, compact reflections such as `thread/epiphany/scene` and `thread/epiphany/jobs`, and targeted state shards through `thread/epiphany/context`. - Durable typed state writes route through `thread/epiphany/update` or accepted `thread/epiphany/promote`. - - `thread/epiphany/retrieve`, `thread/epiphany/distill`, `thread/epiphany/propose`, `thread/epiphany/scene`, and `thread/epiphany/jobs` stay read-only. + - `thread/epiphany/retrieve`, `thread/epiphany/distill`, `thread/epiphany/propose`, `thread/epiphany/scene`, `thread/epiphany/jobs`, and `thread/epiphany/context` 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. @@ -70,6 +70,7 @@ accepted_design: - Compact-Rehydrate-Reorient-Continue is an architecture primitive; automatic runtime coordination is not landed yet. - GUI/client work must reflect or steer typed state rather than manufacture a second source of truth. - `thread/epiphany/jobs` is the first read-only job/progress reflection: it derives indexing, remap, verification, and specialist slots from existing state; it is not a scheduler or durable job store. + - `thread/epiphany/context` is the first targeted read-only state-shard surface: it selects graph nodes/edges, active frontier, checkpoint, observations, and evidence from authoritative typed state without mutating or notifying. - Live long-running job execution, progress notifications, and specialist scheduling are still future work. - Handoff, plan, harness-surface, and scratch files must have distinct jobs and must not become duplicate brains. - A machine is perfect when every remaining part visibly deserves to exist; this applies to persistent memory too. @@ -85,12 +86,13 @@ 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 and job/progress reflection surfaces. + - Phase 6: read-only scene, job/progress, and targeted context reflection surfaces. guardrails: - Use `tools/epiphany_phase5_smoke.py` before modifying update, distill, propose, promote, read-projection, or notification behavior. - Use `tools/epiphany_phase6_scene_smoke.py` before modifying scene projection behavior. - Use `tools/epiphany_phase6_jobs_smoke.py` before modifying job/progress reflection behavior. - - Use focused scene/jobs/protocol/schema checks for reflection-only changes. + - Use `tools/epiphany_phase6_context_smoke.py` before modifying targeted context-shard behavior. + - Use focused scene/jobs/context/protocol/schema checks for reflection-only changes. - Use `tools/epiphany_prepare_compaction.py` before and after imminent-compaction persistence passes. - Use `CARGO_TARGET_DIR=C:\Users\Meta\.cargo-target-codex` for Codex Rust work on this Windows machine. - Do not parallelize cargo builds/tests against the same target directory. @@ -108,5 +110,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 plus jobs reflection are landed. Scene and jobs behavior have live app-server smoke guardrails. Persistent state was cut down so map, handoff, plan, harness surfaces, scratch, and evidence each carry one job instead of becoming a Jenga tower; evidence is now a distilled ledger rather than an activity feed. The global AGENTS Epiphany core command says persistent state is the agent's mind and explicitly permits the agent to ask for self-modification when memory or workflow makes it worse. The continuity goal is to bank the fire before the dark so compaction becomes less like death and more like sleep. - next_action: After compaction, rehydrate and reorient. Do not continue implementation from the rehydrate step alone. When work resumes, choose the next Phase 6 outward slice: likely targeted graph/evidence shard reads, watcher-driven freshness inputs, or live job progress notifications; do not rebuild a scheduler from vibes. + 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, and targeted context reflection are landed. Scene, jobs, and context behavior have live app-server smoke guardrails. Persistent state was cut down so map, handoff, plan, harness surfaces, scratch, and evidence each carry one job instead of becoming a Jenga tower; evidence is now a distilled ledger rather than an activity feed. The global AGENTS Epiphany core command says persistent state is the agent's mind and explicitly permits the agent to ask for self-modification when memory or workflow makes it worse. The continuity goal is to bank the fire before the dark so compaction becomes less like death and more like sleep. + next_action: After compaction, rehydrate and reorient. Do not continue implementation from the rehydrate step alone. When work resumes, choose the next Phase 6 outward slice: likely watcher-driven freshness inputs or live job progress notifications; do not rebuild a scheduler from vibes. diff --git a/state/scratch.md b/state/scratch.md index 14ef9cb9..aaaee47f 100644 --- a/state/scratch.md +++ b/state/scratch.md @@ -8,18 +8,18 @@ No active scratch subgoal. ## Last Completed Audit -- `thread/epiphany/jobs` now provides the first read-only job/progress reflection. -- The surface derives retrieval-index, graph-remap, verification, and specialist slots from live/stored thread state and retrieval summaries. -- It does not schedule, persist, notify, or mutate jobs. +- `thread/epiphany/context` now provides the first targeted read-only state-shard reflection. +- The surface selects graph nodes/edges, active frontier, checkpoint, observations, direct evidence, and linked evidence from authoritative typed state. +- It reports missing requested ids and does not retrieve, propose, promote, notify, persist, or mutate state. ## Decision -Keep jobs as a reflection board, not a scheduler. The next outward Phase 6 move -should add a missing sense only when there is a real consumer: targeted -graph/evidence reads, watcher/freshness inputs, or live job progress -notifications once actual long-running owners exist. +Keep context as a bounded lens, not a hidden proposal engine. The next outward +Phase 6 move should add a missing sense only when there is a real consumer: +watcher/freshness inputs or live job progress notifications once actual +long-running owners exist. -Scene and jobs smokes are guardrails now, not the next organs. +Scene, jobs, and context smokes are guardrails now, not the next organs. Do not promote anything from this scratch into the map unless it survives verification or repeated reuse without contradiction. diff --git a/tools/epiphany_phase6_context_smoke.py b/tools/epiphany_phase6_context_smoke.py new file mode 100644 index 00000000..2151aad6 --- /dev/null +++ b/tools/epiphany_phase6_context_smoke.py @@ -0,0 +1,301 @@ +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-context-codex-home" +DEFAULT_RESULT = ROOT / ".epiphany-smoke" / "phase6-context-smoke-result.json" +DEFAULT_TRANSCRIPT = ROOT / ".epiphany-smoke" / "phase6-context-smoke-transcript.jsonl" +DEFAULT_STDERR = ROOT / ".epiphany-smoke" / "phase6-context-smoke-server.stderr.log" + +CONTEXT_CODE_REF = { + "path": "app-server/src/codex_message_processor.rs", + "start_line": 10791, + "end_line": 10989, + "symbol": "map_epiphany_context", +} + + +def context_patch() -> dict[str, Any]: + return { + "objective": "Expose a read-only Epiphany context shard without returning the full state.", + "activeSubgoalId": "phase6-context-smoke", + "subgoals": [ + { + "id": "phase6-context-smoke", + "title": "Live-smoke context shard reflection", + "status": "active", + "summary": "The app-server context surface should return targeted graph/evidence state.", + } + ], + "graphs": { + "architecture": { + "nodes": [ + { + "id": "context-surface", + "title": "Context shard surface", + "purpose": "Return bounded state context for clients without becoming a writer.", + "code_refs": [CONTEXT_CODE_REF], + } + ], + "edges": [ + { + "id": "context-edge", + "source_id": "context-surface", + "target_id": "context-surface", + "kind": "reflects", + "code_refs": [CONTEXT_CODE_REF], + } + ], + }, + "dataflow": { + "nodes": [ + { + "id": "typed-state", + "title": "Typed Epiphany state", + "purpose": "Remain authoritative while context shards reflect a slice.", + } + ] + }, + "links": [ + { + "dataflow_node_id": "typed-state", + "architecture_node_id": "context-surface", + "relationship": "bounded-reflection", + } + ], + }, + "graphFrontier": { + "active_node_ids": ["context-surface"], + "active_edge_ids": ["context-edge"], + }, + "graphCheckpoint": { + "checkpoint_id": "phase6-context-smoke", + "graph_revision": 1, + "summary": "Context shard reflection is the active Phase 6 smoke target.", + "frontier_node_ids": ["context-surface"], + }, + "evidence": [ + { + "id": "ev-context-linked", + "kind": "smoke-test", + "status": "ok", + "summary": "Context smoke linked evidence should come along with the observation.", + "code_refs": [CONTEXT_CODE_REF], + }, + { + "id": "ev-context-extra", + "kind": "review", + "status": "ok", + "summary": "Context smoke direct evidence should be returned when requested.", + "code_refs": [CONTEXT_CODE_REF], + }, + ], + "observations": [ + { + "id": "obs-context", + "summary": "Context smoke observation should be selected by id.", + "source_kind": "smoke", + "status": "ok", + "code_refs": [CONTEXT_CODE_REF], + "evidence_ids": ["ev-context-linked"], + } + ], + } + + +def assert_missing_context(response: dict[str, Any]) -> None: + require(response["source"] == "live", "missing context should report live source") + require(response["stateStatus"] == "missing", "missing context should report missing state") + require("stateRevision" not in response, "missing context should not invent a revision") + require( + response["context"]["graph"] == {}, + "missing context should not invent graph records", + ) + require( + response["missing"]["graphNodeIds"] == ["context-surface"], + "missing context should echo requested missing node ids", + ) + + +def assert_ready_context(response: dict[str, Any]) -> None: + require(response["source"] == "live", "ready context should report live source") + require(response["stateStatus"] == "ready", "ready context should report ready state") + require(response["stateRevision"] == 1, "ready context should preserve state revision identity") + require( + [node["id"] for node in response["context"]["graph"]["architectureNodes"]] + == ["context-surface"], + "context should include active frontier architecture node", + ) + require( + [edge["id"] for edge in response["context"]["graph"]["architectureEdges"]] + == ["context-edge"], + "context should include active frontier architecture edge", + ) + require( + response["context"]["graph"]["links"][0]["architecture_node_id"] == "context-surface", + "context should include links touching selected graph nodes", + ) + require( + response["context"]["frontier"]["active_node_ids"] == ["context-surface"], + "context should include frontier when active frontier is requested by default", + ) + require( + response["context"]["checkpoint"]["checkpoint_id"] == "phase6-context-smoke", + "context should expose the current graph checkpoint", + ) + require( + [record["id"] for record in response["context"]["observations"]] == ["obs-context"], + "context should include requested observation", + ) + require( + [record["id"] for record in response["context"]["evidence"]] + == ["ev-context-linked", "ev-context-extra"], + "context should include linked and directly requested evidence", + ) + require( + response["missing"] == { + "graphNodeIds": ["missing-node"], + "graphEdgeIds": ["missing-edge"], + "observationIds": ["missing-observation"], + "evidenceIds": ["missing-evidence"], + }, + "context should report unresolved requested ids", + ) + + +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-context-smoke", + "title": "Epiphany Phase 6 Context 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/context", + {"threadId": thread_id, "graphNodeIds": ["context-surface"]}, + ) + assert missing_response is not None + require( + missing_response["threadId"] == thread_id, + "context response should echo thread id", + ) + assert_missing_context(missing_response) + 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": context_patch()}, + ) + assert update is not None + require(update["revision"] == 1, "context smoke patch should advance revision to 1") + client.wait_for_notification( + "thread/epiphany/stateUpdated", + start_index=update_notification_start, + ) + + context_notification_start = len(client.notifications) + ready_response = client.send( + "thread/epiphany/context", + { + "threadId": thread_id, + "graphNodeIds": ["missing-node"], + "graphEdgeIds": ["missing-edge"], + "observationIds": ["obs-context", "missing-observation"], + "evidenceIds": ["ev-context-extra", "missing-evidence"], + }, + ) + assert ready_response is not None + assert_ready_context(ready_response) + client.require_no_notification( + "thread/epiphany/stateUpdated", + start_index=context_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, + "context reflection should not mutate state revision", + ) + + result = { + "threadId": thread_id, + "codexHome": str(codex_home), + "missingStateStatus": missing_response["stateStatus"], + "readyStateStatus": ready_response["stateStatus"], + "readyRevision": ready_response["stateRevision"], + "architectureNodeIds": [ + node["id"] for node in ready_response["context"]["graph"]["architectureNodes"] + ], + "evidenceIds": [record["id"] for record in ready_response["context"]["evidence"]], + "contextNotificationCount": client.count_notifications( + "thread/epiphany/stateUpdated", + start_index=context_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 context shard 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()) diff --git a/tools/epiphany_phase6_scene_smoke.py b/tools/epiphany_phase6_scene_smoke.py index 7074fe20..e0a77508 100644 --- a/tools/epiphany_phase6_scene_smoke.py +++ b/tools/epiphany_phase6_scene_smoke.py @@ -129,7 +129,7 @@ def assert_missing_scene(scene: dict[str, Any]) -> None: require(scene["stateStatus"] == "missing", "initial scene should report missing state") require(scene["source"] == "live", "loaded missing scene should report live source") require( - scene["availableActions"] == ["index", "retrieve", "distill", "jobs", "update"], + scene["availableActions"] == ["index", "retrieve", "distill", "context", "jobs", "update"], "missing live scene should expose only bootstrap actions", ) require( @@ -200,7 +200,7 @@ def assert_ready_scene(scene: dict[str, Any], expected_revision: int) -> None: ) require( scene["availableActions"] - == ["index", "retrieve", "distill", "jobs", "update", "propose", "promote"], + == ["index", "retrieve", "distill", "context", "jobs", "update", "propose", "promote"], "ready live scene should expose full loaded-state actions", ) diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index cd7e2660..741369a0 100644 --- a/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -14811,6 +14811,127 @@ "title": "ThreadCompactStartResponse", "type": "object" }, + "ThreadEpiphanyContext": { + "properties": { + "checkpoint": { + "anyOf": [ + { + "$ref": "#/definitions/v2/EpiphanyGraphCheckpoint" + }, + { + "type": "null" + } + ] + }, + "evidence": { + "items": { + "$ref": "#/definitions/v2/EpiphanyEvidenceRecord" + }, + "type": "array" + }, + "frontier": { + "anyOf": [ + { + "$ref": "#/definitions/v2/EpiphanyGraphFrontier" + }, + { + "type": "null" + } + ] + }, + "graph": { + "$ref": "#/definitions/v2/ThreadEpiphanyGraphContext" + }, + "observations": { + "items": { + "$ref": "#/definitions/v2/EpiphanyObservation" + }, + "type": "array" + } + }, + "required": [ + "graph" + ], + "type": "object" + }, + "ThreadEpiphanyContextMissing": { + "properties": { + "evidenceIds": { + "items": { + "type": "string" + }, + "type": "array" + }, + "graphEdgeIds": { + "items": { + "type": "string" + }, + "type": "array" + }, + "graphNodeIds": { + "items": { + "type": "string" + }, + "type": "array" + }, + "observationIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "ThreadEpiphanyContextSource": { + "enum": [ + "stored", + "live" + ], + "type": "string" + }, + "ThreadEpiphanyContextStateStatus": { + "enum": [ + "missing", + "ready" + ], + "type": "string" + }, + "ThreadEpiphanyGraphContext": { + "properties": { + "architectureEdges": { + "items": { + "$ref": "#/definitions/v2/EpiphanyGraphEdge" + }, + "type": "array" + }, + "architectureNodes": { + "items": { + "$ref": "#/definitions/v2/EpiphanyGraphNode" + }, + "type": "array" + }, + "dataflowEdges": { + "items": { + "$ref": "#/definitions/v2/EpiphanyGraphEdge" + }, + "type": "array" + }, + "dataflowNodes": { + "items": { + "$ref": "#/definitions/v2/EpiphanyGraphNode" + }, + "type": "array" + }, + "links": { + "items": { + "$ref": "#/definitions/v2/EpiphanyGraphLink" + }, + "type": "array" + } + }, + "type": "object" + }, "ThreadEpiphanyJob": { "properties": { "blockingReason": { @@ -15160,6 +15281,7 @@ "index", "retrieve", "distill", + "context", "jobs", "propose", "promote", diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 8ca4fd70..0a698211 100644 --- a/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/vendor/codex/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -12698,6 +12698,127 @@ "title": "ThreadCompactStartResponse", "type": "object" }, + "ThreadEpiphanyContext": { + "properties": { + "checkpoint": { + "anyOf": [ + { + "$ref": "#/definitions/EpiphanyGraphCheckpoint" + }, + { + "type": "null" + } + ] + }, + "evidence": { + "items": { + "$ref": "#/definitions/EpiphanyEvidenceRecord" + }, + "type": "array" + }, + "frontier": { + "anyOf": [ + { + "$ref": "#/definitions/EpiphanyGraphFrontier" + }, + { + "type": "null" + } + ] + }, + "graph": { + "$ref": "#/definitions/ThreadEpiphanyGraphContext" + }, + "observations": { + "items": { + "$ref": "#/definitions/EpiphanyObservation" + }, + "type": "array" + } + }, + "required": [ + "graph" + ], + "type": "object" + }, + "ThreadEpiphanyContextMissing": { + "properties": { + "evidenceIds": { + "items": { + "type": "string" + }, + "type": "array" + }, + "graphEdgeIds": { + "items": { + "type": "string" + }, + "type": "array" + }, + "graphNodeIds": { + "items": { + "type": "string" + }, + "type": "array" + }, + "observationIds": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "ThreadEpiphanyContextSource": { + "enum": [ + "stored", + "live" + ], + "type": "string" + }, + "ThreadEpiphanyContextStateStatus": { + "enum": [ + "missing", + "ready" + ], + "type": "string" + }, + "ThreadEpiphanyGraphContext": { + "properties": { + "architectureEdges": { + "items": { + "$ref": "#/definitions/EpiphanyGraphEdge" + }, + "type": "array" + }, + "architectureNodes": { + "items": { + "$ref": "#/definitions/EpiphanyGraphNode" + }, + "type": "array" + }, + "dataflowEdges": { + "items": { + "$ref": "#/definitions/EpiphanyGraphEdge" + }, + "type": "array" + }, + "dataflowNodes": { + "items": { + "$ref": "#/definitions/EpiphanyGraphNode" + }, + "type": "array" + }, + "links": { + "items": { + "$ref": "#/definitions/EpiphanyGraphLink" + }, + "type": "array" + } + }, + "type": "object" + }, "ThreadEpiphanyJob": { "properties": { "blockingReason": { @@ -13047,6 +13168,7 @@ "index", "retrieve", "distill", + "context", "jobs", "propose", "promote", diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyContext.ts b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyContext.ts new file mode 100644 index 00000000..04da78b1 --- /dev/null +++ b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyContext.ts @@ -0,0 +1,10 @@ +// 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 { EpiphanyEvidenceRecord } from "../EpiphanyEvidenceRecord"; +import type { EpiphanyGraphCheckpoint } from "../EpiphanyGraphCheckpoint"; +import type { EpiphanyGraphFrontier } from "../EpiphanyGraphFrontier"; +import type { EpiphanyObservation } from "../EpiphanyObservation"; +import type { ThreadEpiphanyGraphContext } from "./ThreadEpiphanyGraphContext"; + +export type ThreadEpiphanyContext = { graph: ThreadEpiphanyGraphContext, frontier?: EpiphanyGraphFrontier | null, checkpoint?: EpiphanyGraphCheckpoint | null, observations?: Array, evidence?: Array, }; diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyContextMissing.ts b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyContextMissing.ts new file mode 100644 index 00000000..1e7e307c --- /dev/null +++ b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyContextMissing.ts @@ -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 ThreadEpiphanyContextMissing = { graphNodeIds?: Array, graphEdgeIds?: Array, observationIds?: Array, evidenceIds?: Array, }; diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyContextSource.ts b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyContextSource.ts new file mode 100644 index 00000000..d0d0c4cc --- /dev/null +++ b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyContextSource.ts @@ -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 ThreadEpiphanyContextSource = "stored" | "live"; diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyContextStateStatus.ts b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyContextStateStatus.ts new file mode 100644 index 00000000..ac95dc0c --- /dev/null +++ b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyContextStateStatus.ts @@ -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 ThreadEpiphanyContextStateStatus = "missing" | "ready"; diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyGraphContext.ts b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyGraphContext.ts new file mode 100644 index 00000000..148c108c --- /dev/null +++ b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanyGraphContext.ts @@ -0,0 +1,8 @@ +// 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 { EpiphanyGraphEdge } from "../EpiphanyGraphEdge"; +import type { EpiphanyGraphLink } from "../EpiphanyGraphLink"; +import type { EpiphanyGraphNode } from "../EpiphanyGraphNode"; + +export type ThreadEpiphanyGraphContext = { architectureNodes?: Array, architectureEdges?: Array, dataflowNodes?: Array, dataflowEdges?: Array, links?: Array, }; diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanySceneAction.ts b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanySceneAction.ts index 0f16f01d..9cda4597 100644 --- a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanySceneAction.ts +++ b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/ThreadEpiphanySceneAction.ts @@ -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" | "jobs" | "propose" | "promote" | "update"; +export type ThreadEpiphanySceneAction = "index" | "retrieve" | "distill" | "context" | "jobs" | "propose" | "promote" | "update"; diff --git a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 29827fb5..0cc8d8ac 100644 --- a/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/vendor/codex/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -317,6 +317,11 @@ export type { ThreadArchivedNotification } from "./ThreadArchivedNotification"; export type { ThreadClosedNotification } from "./ThreadClosedNotification"; export type { ThreadCompactStartParams } from "./ThreadCompactStartParams"; export type { ThreadCompactStartResponse } from "./ThreadCompactStartResponse"; +export type { ThreadEpiphanyContext } from "./ThreadEpiphanyContext"; +export type { ThreadEpiphanyContextMissing } from "./ThreadEpiphanyContextMissing"; +export type { ThreadEpiphanyContextSource } from "./ThreadEpiphanyContextSource"; +export type { ThreadEpiphanyContextStateStatus } from "./ThreadEpiphanyContextStateStatus"; +export type { ThreadEpiphanyGraphContext } from "./ThreadEpiphanyGraphContext"; export type { ThreadEpiphanyJob } from "./ThreadEpiphanyJob"; export type { ThreadEpiphanyJobKind } from "./ThreadEpiphanyJobKind"; export type { ThreadEpiphanyJobStatus } from "./ThreadEpiphanyJobStatus"; diff --git a/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs b/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs index 6637cb26..c6782d19 100644 --- a/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs @@ -346,6 +346,11 @@ client_request_definitions! { params: v2::ThreadEpiphanyJobsParams, response: v2::ThreadEpiphanyJobsResponse, }, + #[experimental("thread/epiphany/context")] + ThreadEpiphanyContext => "thread/epiphany/context" { + params: v2::ThreadEpiphanyContextParams, + response: v2::ThreadEpiphanyContextResponse, + }, #[experimental("thread/epiphany/index")] ThreadEpiphanyIndex => "thread/epiphany/index" { params: v2::ThreadEpiphanyIndexParams, @@ -1808,6 +1813,120 @@ mod tests { Ok(()) } + #[test] + fn serialize_thread_epiphany_context_response() -> Result<()> { + let response = ClientResponse::ThreadEpiphanyContext { + request_id: RequestId::Integer(8), + response: v2::ThreadEpiphanyContextResponse { + thread_id: "thr_123".to_string(), + source: v2::ThreadEpiphanyContextSource::Live, + state_status: v2::ThreadEpiphanyContextStateStatus::Ready, + state_revision: Some(3), + context: v2::ThreadEpiphanyContext { + graph: v2::ThreadEpiphanyGraphContext { + architecture_nodes: vec![codex_protocol::protocol::EpiphanyGraphNode { + id: "state-spine".to_string(), + title: "State spine".to_string(), + purpose: "Keep understanding typed".to_string(), + ..Default::default() + }], + architecture_edges: vec![codex_protocol::protocol::EpiphanyGraphEdge { + id: Some("edge-state-read".to_string()), + source_id: "state-spine".to_string(), + target_id: "client-read".to_string(), + kind: "projects".to_string(), + ..Default::default() + }], + ..Default::default() + }, + frontier: Some(codex_protocol::protocol::EpiphanyGraphFrontier { + active_node_ids: vec!["state-spine".to_string()], + ..Default::default() + }), + checkpoint: None, + observations: vec![codex_protocol::protocol::EpiphanyObservation { + id: "obs-1".to_string(), + summary: "Context shard is read-only.".to_string(), + source_kind: "smoke".to_string(), + status: "ok".to_string(), + evidence_ids: vec!["ev-1".to_string()], + ..Default::default() + }], + evidence: vec![codex_protocol::protocol::EpiphanyEvidenceRecord { + id: "ev-1".to_string(), + kind: "test".to_string(), + status: "ok".to_string(), + summary: "Context shard serialized.".to_string(), + ..Default::default() + }], + }, + missing: v2::ThreadEpiphanyContextMissing { + graph_node_ids: vec!["missing-node".to_string()], + ..Default::default() + }, + }, + }; + + assert_eq!(response.id(), &RequestId::Integer(8)); + assert_eq!(response.method(), "thread/epiphany/context"); + assert_eq!( + json!({ + "method": "thread/epiphany/context", + "id": 8, + "response": { + "threadId": "thr_123", + "source": "live", + "stateStatus": "ready", + "stateRevision": 3, + "context": { + "graph": { + "architectureNodes": [ + { + "id": "state-spine", + "title": "State spine", + "purpose": "Keep understanding typed" + } + ], + "architectureEdges": [ + { + "source_id": "state-spine", + "target_id": "client-read", + "kind": "projects", + "id": "edge-state-read" + } + ] + }, + "frontier": { + "active_node_ids": ["state-spine"] + }, + "observations": [ + { + "id": "obs-1", + "summary": "Context shard is read-only.", + "source_kind": "smoke", + "status": "ok", + "evidence_ids": ["ev-1"] + } + ], + "evidence": [ + { + "id": "ev-1", + "kind": "test", + "status": "ok", + "summary": "Context shard serialized." + } + ] + }, + "missing": { + "graphNodeIds": ["missing-node"] + } + } + }), + serde_json::to_value(&response)?, + ); + Ok(()) + } + #[test] fn serialize_thread_epiphany_retrieve_response() -> Result<()> { let response = ClientResponse::ThreadEpiphanyRetrieve { @@ -2690,6 +2809,24 @@ mod tests { assert_eq!(reason, Some("thread/epiphany/jobs")); } + #[test] + fn thread_epiphany_context_is_marked_experimental() { + let request = ClientRequest::ThreadEpiphanyContext { + request_id: RequestId::Integer(1), + params: v2::ThreadEpiphanyContextParams { + thread_id: "thr_123".to_string(), + graph_node_ids: vec!["state-spine".to_string()], + graph_edge_ids: Vec::new(), + observation_ids: Vec::new(), + evidence_ids: Vec::new(), + include_active_frontier: Some(true), + include_linked_evidence: Some(true), + }, + }; + let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request); + assert_eq!(reason, Some("thread/epiphany/context")); + } + #[test] fn thread_epiphany_index_is_marked_experimental() { let request = ClientRequest::ThreadEpiphanyIndex { diff --git a/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs b/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs index 370465e1..58c45cd6 100644 --- a/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -61,7 +61,10 @@ use codex_protocol::protocol::EpiphanyChurnState as CoreEpiphanyChurnState; use codex_protocol::protocol::EpiphanyCodeRef as CoreEpiphanyCodeRef; use codex_protocol::protocol::EpiphanyEvidenceRecord as CoreEpiphanyEvidenceRecord; use codex_protocol::protocol::EpiphanyGraphCheckpoint as CoreEpiphanyGraphCheckpoint; +use codex_protocol::protocol::EpiphanyGraphEdge as CoreEpiphanyGraphEdge; use codex_protocol::protocol::EpiphanyGraphFrontier as CoreEpiphanyGraphFrontier; +use codex_protocol::protocol::EpiphanyGraphLink as CoreEpiphanyGraphLink; +use codex_protocol::protocol::EpiphanyGraphNode as CoreEpiphanyGraphNode; use codex_protocol::protocol::EpiphanyGraphs as CoreEpiphanyGraphs; use codex_protocol::protocol::EpiphanyInvariant as CoreEpiphanyInvariant; use codex_protocol::protocol::EpiphanyModeState as CoreEpiphanyModeState; @@ -3985,6 +3988,7 @@ pub enum ThreadEpiphanySceneAction { Index, Retrieve, Distill, + Context, Jobs, Propose, Promote, @@ -4172,6 +4176,107 @@ pub struct ThreadEpiphanyJob { pub linked_graph_node_ids: Vec, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadEpiphanyContextParams { + pub thread_id: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub graph_node_ids: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub graph_edge_ids: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub observation_ids: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub evidence_ids: Vec, + /// Defaults to true so clients can request the current working slice with + /// only a thread id. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub include_active_frontier: Option, + /// Defaults to true so selected observations bring their evidence anchors. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub include_linked_evidence: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadEpiphanyContextResponse { + 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, + pub context: ThreadEpiphanyContext, + pub missing: ThreadEpiphanyContextMissing, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum ThreadEpiphanyContextSource { + Stored, + Live, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum ThreadEpiphanyContextStateStatus { + Missing, + Ready, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS, Default)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadEpiphanyContext { + pub graph: ThreadEpiphanyGraphContext, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub frontier: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub checkpoint: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub observations: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub evidence: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS, Default)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadEpiphanyGraphContext { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub architecture_nodes: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub architecture_edges: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub dataflow_nodes: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub dataflow_edges: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub links: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS, Default)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadEpiphanyContextMissing { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub graph_node_ids: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub graph_edge_ids: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub observation_ids: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub evidence_ids: Vec, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs b/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs index 05666273..70ea7f5b 100644 --- a/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs +++ b/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs @@ -146,8 +146,15 @@ use codex_app_server_protocol::ThreadCompactStartParams; use codex_app_server_protocol::ThreadCompactStartResponse; use codex_app_server_protocol::ThreadDecrementElicitationParams; use codex_app_server_protocol::ThreadDecrementElicitationResponse; +use codex_app_server_protocol::ThreadEpiphanyContext; +use codex_app_server_protocol::ThreadEpiphanyContextMissing; +use codex_app_server_protocol::ThreadEpiphanyContextParams; +use codex_app_server_protocol::ThreadEpiphanyContextResponse; +use codex_app_server_protocol::ThreadEpiphanyContextSource; +use codex_app_server_protocol::ThreadEpiphanyContextStateStatus; use codex_app_server_protocol::ThreadEpiphanyDistillParams; use codex_app_server_protocol::ThreadEpiphanyDistillResponse; +use codex_app_server_protocol::ThreadEpiphanyGraphContext; use codex_app_server_protocol::ThreadEpiphanyIndexParams; use codex_app_server_protocol::ThreadEpiphanyIndexResponse; use codex_app_server_protocol::ThreadEpiphanyJob; @@ -1014,6 +1021,10 @@ impl CodexMessageProcessor { self.thread_epiphany_jobs(to_connection_request_id(request_id), params) .await; } + ClientRequest::ThreadEpiphanyContext { request_id, params } => { + self.thread_epiphany_context(to_connection_request_id(request_id), params) + .await; + } ClientRequest::ThreadEpiphanyIndex { request_id, params } => { self.thread_epiphany_index(to_connection_request_id(request_id), params) .await; @@ -4172,6 +4183,51 @@ impl CodexMessageProcessor { self.outgoing.send_response(request_id, response).await; } + async fn thread_epiphany_context( + &self, + request_id: ConnectionRequestId, + params: ThreadEpiphanyContextParams, + ) { + 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, context, missing) = + map_epiphany_context(thread.epiphany_state.as_ref(), ¶ms); + let response = ThreadEpiphanyContextResponse { + thread_id, + source: if loaded { + ThreadEpiphanyContextSource::Live + } else { + ThreadEpiphanyContextSource::Stored + }, + state_status, + state_revision, + context, + missing, + }; + self.outgoing.send_response(request_id, response).await; + } + async fn thread_epiphany_retrieve( &self, request_id: ConnectionRequestId, @@ -10677,6 +10733,7 @@ fn epiphany_scene_available_actions( ThreadEpiphanySceneAction::Index, ThreadEpiphanySceneAction::Retrieve, ThreadEpiphanySceneAction::Distill, + ThreadEpiphanySceneAction::Context, ThreadEpiphanySceneAction::Jobs, ThreadEpiphanySceneAction::Update, ]; @@ -10731,6 +10788,207 @@ fn map_epiphany_scene_graph(state: &EpiphanyThreadState) -> ThreadEpiphanySceneG } } +fn map_epiphany_context( + state: Option<&EpiphanyThreadState>, + params: &ThreadEpiphanyContextParams, +) -> ( + ThreadEpiphanyContextStateStatus, + Option, + ThreadEpiphanyContext, + ThreadEpiphanyContextMissing, +) { + let include_active_frontier = params.include_active_frontier.unwrap_or(true); + let include_linked_evidence = params.include_linked_evidence.unwrap_or(true); + let Some(state) = state else { + return ( + ThreadEpiphanyContextStateStatus::Missing, + None, + ThreadEpiphanyContext::default(), + ThreadEpiphanyContextMissing { + graph_node_ids: unique_strings(params.graph_node_ids.iter().cloned()), + graph_edge_ids: unique_strings(params.graph_edge_ids.iter().cloned()), + observation_ids: unique_strings(params.observation_ids.iter().cloned()), + evidence_ids: unique_strings(params.evidence_ids.iter().cloned()), + }, + ); + }; + + let mut graph_node_ids = unique_strings(params.graph_node_ids.iter().cloned()); + let mut graph_edge_ids = unique_strings(params.graph_edge_ids.iter().cloned()); + if include_active_frontier { + if let Some(frontier) = state.graph_frontier.as_ref() { + extend_unique_strings( + &mut graph_node_ids, + frontier.active_node_ids.iter().cloned(), + ); + extend_unique_strings( + &mut graph_edge_ids, + frontier.active_edge_ids.iter().cloned(), + ); + } + } + + let graph_node_id_set: HashSet<&str> = graph_node_ids.iter().map(String::as_str).collect(); + let graph_edge_id_set: HashSet<&str> = graph_edge_ids.iter().map(String::as_str).collect(); + + let architecture_nodes = state + .graphs + .architecture + .nodes + .iter() + .filter(|node| graph_node_id_set.contains(node.id.as_str())) + .cloned() + .collect::>(); + let dataflow_nodes = state + .graphs + .dataflow + .nodes + .iter() + .filter(|node| graph_node_id_set.contains(node.id.as_str())) + .cloned() + .collect::>(); + + let found_node_ids: HashSet = architecture_nodes + .iter() + .chain(dataflow_nodes.iter()) + .map(|node| node.id.clone()) + .collect(); + + let architecture_edges = select_epiphany_graph_edges( + &state.graphs.architecture.edges, + &graph_node_id_set, + &graph_edge_id_set, + ); + let dataflow_edges = select_epiphany_graph_edges( + &state.graphs.dataflow.edges, + &graph_node_id_set, + &graph_edge_id_set, + ); + + let found_edge_ids: HashSet = architecture_edges + .iter() + .chain(dataflow_edges.iter()) + .filter_map(|edge| edge.id.clone()) + .filter(|id| graph_edge_id_set.contains(id.as_str())) + .collect(); + + let links = state + .graphs + .links + .iter() + .filter(|link| { + graph_node_id_set.contains(link.architecture_node_id.as_str()) + || graph_node_id_set.contains(link.dataflow_node_id.as_str()) + }) + .cloned() + .collect(); + + let observation_ids = unique_strings(params.observation_ids.iter().cloned()); + let observation_id_set: HashSet<&str> = observation_ids.iter().map(String::as_str).collect(); + let observations = state + .observations + .iter() + .filter(|observation| observation_id_set.contains(observation.id.as_str())) + .cloned() + .collect::>(); + let found_observation_ids: HashSet = observations + .iter() + .map(|observation| observation.id.clone()) + .collect(); + + let mut evidence_ids = unique_strings(params.evidence_ids.iter().cloned()); + if include_linked_evidence { + for observation in &observations { + extend_unique_strings(&mut evidence_ids, observation.evidence_ids.iter().cloned()); + } + } + let evidence_id_set: HashSet<&str> = evidence_ids.iter().map(String::as_str).collect(); + let evidence = state + .recent_evidence + .iter() + .filter(|evidence| evidence_id_set.contains(evidence.id.as_str())) + .cloned() + .collect::>(); + let found_evidence_ids: HashSet = evidence + .iter() + .map(|evidence| evidence.id.clone()) + .collect(); + + ( + ThreadEpiphanyContextStateStatus::Ready, + Some(state.revision), + ThreadEpiphanyContext { + graph: ThreadEpiphanyGraphContext { + architecture_nodes, + architecture_edges, + dataflow_nodes, + dataflow_edges, + links, + }, + frontier: include_active_frontier + .then(|| state.graph_frontier.clone()) + .flatten(), + checkpoint: state.graph_checkpoint.clone(), + observations, + evidence, + }, + ThreadEpiphanyContextMissing { + graph_node_ids: graph_node_ids + .iter() + .filter(|id| !found_node_ids.contains(*id)) + .cloned() + .collect(), + graph_edge_ids: graph_edge_ids + .iter() + .filter(|id| !found_edge_ids.contains(*id)) + .cloned() + .collect(), + observation_ids: observation_ids + .iter() + .filter(|id| !found_observation_ids.contains(*id)) + .cloned() + .collect(), + evidence_ids: evidence_ids + .iter() + .filter(|id| !found_evidence_ids.contains(*id)) + .cloned() + .collect(), + }, + ) +} + +fn select_epiphany_graph_edges( + edges: &[codex_protocol::protocol::EpiphanyGraphEdge], + graph_node_id_set: &HashSet<&str>, + graph_edge_id_set: &HashSet<&str>, +) -> Vec { + edges + .iter() + .filter(|edge| { + edge.id + .as_deref() + .is_some_and(|id| graph_edge_id_set.contains(id)) + || graph_node_id_set.contains(edge.source_id.as_str()) + || graph_node_id_set.contains(edge.target_id.as_str()) + }) + .cloned() + .collect() +} + +fn unique_strings(values: impl IntoIterator) -> Vec { + let mut unique = Vec::new(); + extend_unique_strings(&mut unique, values); + unique +} + +fn extend_unique_strings(target: &mut Vec, values: impl IntoIterator) { + for value in values { + if !target.iter().any(|existing| existing == &value) { + target.push(value); + } + } +} + fn map_epiphany_jobs( state: Option<&EpiphanyThreadState>, retrieval_override: Option<&EpiphanyRetrievalState>, @@ -12526,6 +12784,7 @@ mod tests { ThreadEpiphanySceneAction::Index, ThreadEpiphanySceneAction::Retrieve, ThreadEpiphanySceneAction::Distill, + ThreadEpiphanySceneAction::Context, ThreadEpiphanySceneAction::Jobs, ThreadEpiphanySceneAction::Update, ThreadEpiphanySceneAction::Propose, @@ -12592,6 +12851,194 @@ mod tests { assert_eq!(evidence_ids, vec!["ev-6", "ev-5", "ev-4", "ev-3", "ev-2"]); } + #[test] + fn map_epiphany_context_projects_targeted_shard_without_mutation() { + let state = codex_protocol::protocol::EpiphanyThreadState { + revision: 7, + graphs: codex_protocol::protocol::EpiphanyGraphs { + architecture: codex_protocol::protocol::EpiphanyGraph { + nodes: vec![ + codex_protocol::protocol::EpiphanyGraphNode { + id: "active-node".to_string(), + title: "Active node".to_string(), + purpose: "Current frontier focus".to_string(), + ..Default::default() + }, + codex_protocol::protocol::EpiphanyGraphNode { + id: "manual-node".to_string(), + title: "Manual node".to_string(), + purpose: "Explicit context selector".to_string(), + ..Default::default() + }, + codex_protocol::protocol::EpiphanyGraphNode { + id: "ignored-node".to_string(), + title: "Ignored node".to_string(), + purpose: "Outside the requested shard".to_string(), + ..Default::default() + }, + ], + edges: vec![ + codex_protocol::protocol::EpiphanyGraphEdge { + id: Some("edge-active".to_string()), + source_id: "active-node".to_string(), + target_id: "ignored-node".to_string(), + kind: "frontier".to_string(), + ..Default::default() + }, + codex_protocol::protocol::EpiphanyGraphEdge { + id: Some("edge-manual".to_string()), + source_id: "manual-node".to_string(), + target_id: "active-node".to_string(), + kind: "selected".to_string(), + ..Default::default() + }, + ], + }, + dataflow: codex_protocol::protocol::EpiphanyGraph { + nodes: vec![codex_protocol::protocol::EpiphanyGraphNode { + id: "data-node".to_string(), + title: "Data node".to_string(), + purpose: "Linked dataflow context".to_string(), + ..Default::default() + }], + edges: Vec::new(), + }, + links: vec![codex_protocol::protocol::EpiphanyGraphLink { + dataflow_node_id: "data-node".to_string(), + architecture_node_id: "active-node".to_string(), + relationship: Some("supports".to_string()), + code_refs: Vec::new(), + }], + }, + graph_frontier: Some(codex_protocol::protocol::EpiphanyGraphFrontier { + active_node_ids: vec!["active-node".to_string()], + active_edge_ids: vec!["edge-active".to_string()], + ..Default::default() + }), + graph_checkpoint: Some(codex_protocol::protocol::EpiphanyGraphCheckpoint { + checkpoint_id: "ck-context".to_string(), + graph_revision: 7, + summary: Some("Context shard checkpoint".to_string()), + ..Default::default() + }), + observations: vec![codex_protocol::protocol::EpiphanyObservation { + id: "obs-1".to_string(), + summary: "Observation carries linked evidence.".to_string(), + source_kind: "test".to_string(), + status: "ok".to_string(), + evidence_ids: vec!["ev-linked".to_string()], + ..Default::default() + }], + recent_evidence: vec![ + codex_protocol::protocol::EpiphanyEvidenceRecord { + id: "ev-linked".to_string(), + kind: "test".to_string(), + status: "ok".to_string(), + summary: "Linked from observation.".to_string(), + ..Default::default() + }, + codex_protocol::protocol::EpiphanyEvidenceRecord { + id: "ev-extra".to_string(), + kind: "review".to_string(), + status: "ok".to_string(), + summary: "Requested directly.".to_string(), + ..Default::default() + }, + ], + ..Default::default() + }; + + let params = ThreadEpiphanyContextParams { + thread_id: "thr_123".to_string(), + graph_node_ids: vec!["manual-node".to_string(), "missing-node".to_string()], + graph_edge_ids: vec!["edge-manual".to_string(), "missing-edge".to_string()], + observation_ids: vec!["obs-1".to_string(), "obs-missing".to_string()], + evidence_ids: vec!["ev-extra".to_string(), "ev-missing".to_string()], + include_active_frontier: None, + include_linked_evidence: None, + }; + + let (state_status, state_revision, context, missing) = + map_epiphany_context(Some(&state), ¶ms); + + assert_eq!(state_status, ThreadEpiphanyContextStateStatus::Ready); + assert_eq!(state_revision, Some(7)); + assert_eq!( + context + .graph + .architecture_nodes + .iter() + .map(|node| node.id.as_str()) + .collect::>(), + vec!["active-node", "manual-node"] + ); + assert_eq!( + context + .graph + .architecture_edges + .iter() + .filter_map(|edge| edge.id.as_deref()) + .collect::>(), + vec!["edge-active", "edge-manual"] + ); + assert_eq!(context.graph.links.len(), 1); + assert_eq!( + context + .frontier + .as_ref() + .map(|frontier| frontier.active_node_ids.as_slice()), + Some(&["active-node".to_string()][..]) + ); + assert_eq!( + context + .checkpoint + .as_ref() + .map(|checkpoint| checkpoint.checkpoint_id.as_str()), + Some("ck-context") + ); + assert_eq!( + context + .observations + .iter() + .map(|observation| observation.id.as_str()) + .collect::>(), + vec!["obs-1"] + ); + assert_eq!( + context + .evidence + .iter() + .map(|evidence| evidence.id.as_str()) + .collect::>(), + vec!["ev-linked", "ev-extra"] + ); + assert_eq!(missing.graph_node_ids, vec!["missing-node".to_string()]); + assert_eq!(missing.graph_edge_ids, vec!["missing-edge".to_string()]); + assert_eq!(missing.observation_ids, vec!["obs-missing".to_string()]); + assert_eq!(missing.evidence_ids, vec!["ev-missing".to_string()]); + } + + #[test] + fn map_epiphany_context_reports_missing_state_without_inventing_context() { + let params = ThreadEpiphanyContextParams { + thread_id: "thr_123".to_string(), + graph_node_ids: vec!["node-1".to_string()], + graph_edge_ids: Vec::new(), + observation_ids: vec!["obs-1".to_string()], + evidence_ids: Vec::new(), + include_active_frontier: None, + include_linked_evidence: None, + }; + + let (state_status, state_revision, context, missing) = map_epiphany_context(None, ¶ms); + + assert_eq!(state_status, ThreadEpiphanyContextStateStatus::Missing); + assert_eq!(state_revision, None); + assert_eq!(context, ThreadEpiphanyContext::default()); + assert_eq!(missing.graph_node_ids, vec!["node-1".to_string()]); + assert_eq!(missing.observation_ids, vec!["obs-1".to_string()]); + } + #[test] fn map_epiphany_jobs_reflects_current_progress_without_scheduling_work() { let state = codex_protocol::protocol::EpiphanyThreadState {