Add Epiphany reorient launch surface

This commit is contained in:
Meta Vens 2026-04-26 17:31:36 +01:00
commit ca3c7c4e33
15 changed files with 1006 additions and 28 deletions

View file

@ -29,8 +29,9 @@ The current spine is real, not aspirational:
- successful-write notifications through `thread/epiphany/stateUpdated`
- durable `jobBindings` as a thin Epiphany-owned launcher seam over runtime job backends
- explicit launch and interrupt authority through `thread/epiphany/jobLaunch` and `thread/epiphany/jobInterrupt`, still backed by runtime `agent_jobs`
- a bounded reorient-guided specialist launch through `thread/epiphany/reorientLaunch`
- live bound-job progress notifications through `thread/epiphany/jobsUpdated`
- read-only reflection surfaces through `thread/epiphany/scene`, `jobs`, `freshness`, `context`, and `pressure`
- read-only reflection surfaces through `thread/epiphany/scene`, `jobs`, `freshness`, `context`, `pressure`, and `reorient`
- durable investigation checkpointing for compaction-safe planning
- repo-owned heavy Epiphany organs in `epiphany-core/`, with vendored Codex kept as the host seam where practical

View file

@ -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/jobLaunch` | bounded authority write | landed, live-smoked | Creates a launcher-owned durable `jobBinding`, launches the current backend adapter, and emits `stateUpdated` with source `jobLaunch`. |
| `thread/epiphany/jobInterrupt` | bounded authority write | landed, live-smoked | Interrupts the current backend adapter for a bound launcher job, clears backend identity from the durable `jobBinding`, and emits `stateUpdated` with source `jobInterrupt`. |
| `thread/epiphany/reorientLaunch` | bounded authority write | landed, live-smoked | Consumes the read-only reorientation verdict and launches one fixed `reorient-specialist` worker with explicit resume/regather scope over the current backend adapter. |
| `thread/epiphany/jobsUpdated` | notification | landed | Emits changed launcher-bound job snapshots for real runtime progress events when the mapped payload actually changes. |
| `thread/epiphany/scene` | read-only reflection | landed, live-smoked | Compact client scene derived from authoritative Epiphany state, including checkpoint summary reflection. |
| `thread/epiphany/jobs` | read-only reflection | landed, live-smoked | Derived indexing, remap, verification, and specialist-progress slots from typed state and retrieval summaries, with durable launcher metadata plus live backend overlay when a real owner exists. |
@ -66,6 +67,7 @@ Durable Epiphany state may change through:
- accepted `thread/epiphany/promote`
- `thread/epiphany/jobLaunch`
- `thread/epiphany/jobInterrupt`
- `thread/epiphany/reorientLaunch`
- normal rollout persistence of the current live `EpiphanyThreadState`
The following must stay read-only:
@ -163,6 +165,13 @@ adapter, but only to create or interrupt explicit launcher-owned work. They
are not a queue, a second scheduler, or permission to smuggle runtime policy
into the reflection surfaces.
The first bounded runtime consumer over CRRC verdicts is also landed as
`thread/epiphany/reorientLaunch`. It can only launch one fixed
`reorient-specialist` binding through the same backend adapter, with explicit
resume-versus-regather scope and checkpoint-derived payload. It is not
automatic CRRC, not a background coordinator, and not a license to keep coding
after drift without an explicit launch.
The first live bound-runtime progress notification is also landed as
`thread/epiphany/jobsUpdated`. It rides existing `agent_job_progress:{json}`
background events from the runtime job runner, resolves matching launcher
@ -257,6 +266,11 @@ It returns a read-only verdict:
It does not mutate `SessionState`, compact, schedule, notify `thread/epiphany/stateUpdated`, auto-resume work, or auto-regather source. It is the wakeup verdict, not the hand reaching for the keyboard.
The first explicit hand reaching for the keyboard is now
`thread/epiphany/reorientLaunch`, and even that hand stays on a short leash:
one fixed specialist role, one explicit launch call, one bounded checkpoint
packet, and no ambient runtime coordination.
## Compaction And CRRC
Compact-Rehydrate-Reorient-Continue is an architecture primitive, but automatic
@ -282,11 +296,14 @@ signal now exists as `thread/epiphany/freshness`, live watcher-backed
invalidation telemetry now exists inside that freshness surface for loaded
threads, durable investigation packets now exist in typed state plus
prompt/scene/context reflection, the first bounded policy verdict now exists as
`thread/epiphany/reorient`, and explicit launch/interrupt authority now exists
over the thin job seam. What does not exist yet is the runtime coordinator that
acts on that verdict. Automatic CRRC still needs explicit runtime policy,
bounded job ownership, and clean stopping rules instead of vibes with a
clipboard.
`thread/epiphany/reorient`, explicit launch/interrupt authority now exists over
the thin job seam, and one explicit `thread/epiphany/reorientLaunch` consumer
can now act on that verdict without becoming a hidden scheduler. What does not
exist yet is the read-back path that brings that specialist's findings back
into typed state or reflection, nor the automatic runtime coordinator that
decides when to launch it. Automatic CRRC still needs explicit runtime policy,
bounded job ownership, clean stopping rules, and honest result plumbing instead
of vibes with a clipboard.
Compaction should squeeze scratch, not the map.

View file

@ -10,7 +10,7 @@ The important question for this file is not "what is Epiphany supposed to become
Current Epiphany is a forked Codex harness with a typed modeling spine.
The spine exists in eighteen live paths:
The spine exists in nineteen 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).
@ -30,6 +30,7 @@ The spine exists in eighteen live paths:
- targeted context reflection: app-server declares experimental read-only `thread/epiphany/context`, reads the same live/stored Epiphany state view used by scene/jobs, and derives a bounded state shard containing selected graph nodes/edges, links, active frontier, checkpoint, the full durable investigation-checkpoint packet, observations, direct evidence, linked evidence, and missing requested ids without retrieval, proposal, promotion, notification, mutation, or persistence in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:355), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4253), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4261), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11152).
- context-pressure reflection: core token telemetry carries `model_auto_compact_token_limit` beside total/last usage and `model_context_window`; app-server declares experimental read-only `thread/epiphany/pressure`, reads live token usage or the latest stored rollout `TokenCount`, and derives `unknown`/`low`/`elevated`/`high`/`critical` pressure plus a non-acting compaction-prep recommendation without mutating Epiphany state or scheduling compaction in [protocol.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/protocol/src/protocol.rs:2237), [turn_context.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/session/turn_context.rs:103), [session/mod.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/session/mod.rs:2716), [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:360), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4361), [token_usage_replay.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor/token_usage_replay.rs:75), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4306), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11028).
- reorientation policy: app-server declares experimental read-only `thread/epiphany/reorient`, reuses the same live/stored thread view plus mapped pressure/freshness/watcher signals, and derives a bounded `resume`/`regather` verdict with checkpoint-path/frontier reasons without mutating, compacting, scheduling, notifying, or continuing work in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:365), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4514), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4394), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11424).
- reorientation launch: app-server now declares experimental `thread/epiphany/reorientLaunch`, requires a loaded thread plus durable investigation checkpoint, consumes the live reorientation verdict, and launches one fixed `reorient-specialist` binding through the existing job-control seam with explicit resume-versus-regather scope, checkpoint-derived payload, and structured-output expectations instead of pretending CRRC suddenly grew a scheduler in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:370), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4575), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4519).
- retrieval/indexing: app-server routes `thread/epiphany/retrieve` and `thread/epiphany/index` through loaded `CodexThread` host methods into `epiphany-core` in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4356), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4432), [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:467), and [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:449).
The thick Epiphany-owned implementation is mostly outside vendored Codex now:
@ -79,6 +80,7 @@ flowchart TD
QF --> QR["thread/epiphany/reorient derives read-only resume/regather verdict"]
QQ --> QR
QR --> Q
QR --> QRL["thread/epiphany/reorientLaunch can explicitly consume that verdict"]
A --> R["thread/epiphany/retrieve requires loaded thread"]
A --> S["thread/epiphany/index requires loaded thread"]
A --> AA["thread/epiphany/distill requires loaded thread"]
@ -87,6 +89,7 @@ flowchart TD
A --> Z["thread/epiphany/update requires loaded thread"]
A --> AJL["thread/epiphany/jobLaunch requires loaded thread"]
A --> AJI["thread/epiphany/jobInterrupt requires loaded thread"]
A --> QRLQ["thread/epiphany/reorientLaunch requires loaded thread"]
AA --> AB["epiphany-core distill_observation"]
AB --> AC["Observation/evidence patch proposal"]
AC --> AD
@ -102,6 +105,8 @@ flowchart TD
S --> U["CodexThread.epiphany_index"]
Z --> ZA["CodexThread.epiphany_update_state"]
AJL --> AJLA["CodexThread.epiphany_launch_job"]
QRLQ --> QRLA["Map checkpoint + reorient verdict into fixed reorient-specialist launch"]
QRLA --> AJLA
AJLA --> AJLB["Persist launcher-bound jobBinding + create backend job"]
AJLB --> ZD
AJI --> AJIA["CodexThread.epiphany_interrupt_job"]
@ -1309,6 +1314,7 @@ stored Epiphany state
-> reflected as targeted read-only graph/evidence context shards for clients
-> reflected as read-only context pressure from token telemetry
-> reflected as a read-only resume/regather verdict from checkpoint + freshness + watcher + pressure
-> can be consumed explicitly by one bounded reorient-specialist launch surface
-> retrieval/indexing can inform future work
-> explicit distillation can draft observation/evidence patches
-> explicit proposal can draft map/churn patches from verified observations
@ -1317,7 +1323,7 @@ stored Epiphany state
-> successful writes emit thread/epiphany/stateUpdated
```
The current remaining missing organ is not the red pen, not the first chewing motion, not the first state-change bell, not the first client-shaped scene, not the first job/progress reflection, not the first live bound-job notification, not the first freshness lens, not the first watcher-backed invalidation read, not the first targeted graph/evidence context read, not the first context-pressure gauge, not the first durable investigation checkpoint packet, not the first bounded reorientation verdict, not the first thin launcher seam, and no longer the first explicit launch/interrupt authority surface over that seam. Those exist. Proposal now has the first bits of map memory, focus, and selection hygiene: it can auto-select a bounded evidence-backed observation cluster when ids are omitted, requires accepting recent evidence behind selected observations, prioritizes stronger selected observations for proposal wording, reuses existing architecture nodes by concrete code-ref/path/id checks before creating new path nodes, can rescue unanchored graph nodes through strict unique semantic overlap, follows graph links and incident edges into the frontier, and reports match-kind-aware churn pressure from the actual proposal shape. Distillation now also has the first source-output-aware teeth for tool/model summaries. Promotion now notices risky deltas instead of just checking the shape of the envelope, treats graph expansion as risky even if the patch tries to mumble "low pressure" while dragging a new wall into the blueprint, token-matches strong verifier kinds so `contest` cannot sneak past as `test`, and validates investigation checkpoints against known evidence before the packet can be inked. Successful update/promote writes now emit `thread/epiphany/stateUpdated` with `source: "update"` or `source: "promote"`, event-level revision, and typed changed fields so clients no longer need to poll blindly, infer the cause from timing, or diff the whole state to know which top-level sections moved. Runtime `agent_job_progress` background events now also emit `thread/epiphany/jobsUpdated` with changed bound-job payloads so clients no longer need to poll `thread/epiphany/jobs` just to watch one real owner move, the bound jobs now reflect launcher id, authority scope, and backend kind/job id instead of treating raw runtime pointers as the architecture, and the explicit `jobLaunch` / `jobInterrupt` surfaces can now create or tear down those bindings without pretending reflection is authority. The scene, jobs, freshness, context, pressure, and reorient surfaces give clients compact or bounded read-only views without making the GUI/client authoritative, scene/context surface the checkpoint packet back out without becoming writers, freshness surfaces watcher telemetry without becoming a hidden worker, and reorient now turns those signals into an explicit wakeup verdict. The remaining missing organs are a second backend if `agent_jobs` stops being enough, a runtime consumer that can act on the reorientation verdict without becoming a hidden tyrant, and eventually role-scoped specialist ownership without silently auto-promoting anything.
The current remaining missing organ is not the red pen, not the first chewing motion, not the first state-change bell, not the first client-shaped scene, not the first job/progress reflection, not the first live bound-job notification, not the first freshness lens, not the first watcher-backed invalidation read, not the first targeted graph/evidence context read, not the first context-pressure gauge, not the first durable investigation checkpoint packet, not the first bounded reorientation verdict, not the first thin launcher seam, not the first explicit launch/interrupt authority surface over that seam, and no longer the first runtime consumer that can act on the reorientation verdict without becoming a hidden tyrant. Those exist. Proposal now has the first bits of map memory, focus, and selection hygiene: it can auto-select a bounded evidence-backed observation cluster when ids are omitted, requires accepting recent evidence behind selected observations, prioritizes stronger selected observations for proposal wording, reuses existing architecture nodes by concrete code-ref/path/id checks before creating new path nodes, can rescue unanchored graph nodes through strict unique semantic overlap, follows graph links and incident edges into the frontier, and reports match-kind-aware churn pressure from the actual proposal shape. Distillation now also has the first source-output-aware teeth for tool/model summaries. Promotion now notices risky deltas instead of just checking the shape of the envelope, treats graph expansion as risky even if the patch tries to mumble "low pressure" while dragging a new wall into the blueprint, token-matches strong verifier kinds so `contest` cannot sneak past as `test`, and validates investigation checkpoints against known evidence before the packet can be inked. Successful update/promote writes now emit `thread/epiphany/stateUpdated` with `source: "update"` or `source: "promote"`, event-level revision, and typed changed fields so clients no longer need to poll blindly, infer the cause from timing, or diff the whole state to know which top-level sections moved. Runtime `agent_job_progress` background events now also emit `thread/epiphany/jobsUpdated` with changed bound-job payloads so clients no longer need to poll `thread/epiphany/jobs` just to watch one real owner move, the bound jobs now reflect launcher id, authority scope, and backend kind/job id instead of treating raw runtime pointers as the architecture, the explicit `jobLaunch` / `jobInterrupt` surfaces can now create or tear down those bindings without pretending reflection is authority, and `reorientLaunch` can now launch one fixed `reorient-specialist` worker from the checkpoint verdict without turning the verdict itself into a writer. The scene, jobs, freshness, context, pressure, and reorient surfaces give clients compact or bounded read-only views without making the GUI/client authoritative, scene/context surface the checkpoint packet back out without becoming writers, freshness surfaces watcher telemetry without becoming a hidden worker, and reorient now turns those signals into an explicit wakeup verdict that one explicit launch surface can consume on purpose. The remaining missing organs are a second backend if `agent_jobs` stops being enough, a read-back path that brings reorientation-specialist findings back into typed state or reflection without auto-promoting them, and eventually role-scoped specialist ownership without silently auto-promoting anything.
```text
model/tool observations
@ -1331,7 +1337,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, investigation-checkpoint, or churn fields, that try to promote risky churn without a warning and stronger verifier evidence, that claim a graph expansion is low-pressure to dodge the warning gate, or that rely on substring verifier-kind accidents. The first live state notification layer also exists: accepted writes publish updated typed state plus event-level revision and typed changed fields; rejected promotions stay quiet.
In natural language: current Epiphany can preserve a map, show a map, project a compact scene from the map, project derived job/progress slots from the map and retrieval summary, reflect a thin launcher seam over bound work, explicitly launch or interrupt that bound work through a bounded authority surface, emit live bound-job progress notifications from runtime `agent_job_progress` events, project a read-only freshness lens over retrieval and graph staleness, layer watcher-backed invalidation telemetry onto that lens for loaded threads, project a targeted graph/evidence context shard from the map, project context pressure from token telemetry, bank an in-flight investigation packet in durable typed state, reflect that packet back into prompt/scene/context, turn those signals into a read-only resume-versus-regather verdict, retrieve evidence for the map, draft one explicit observation/evidence patch, derive a bounded graph/frontier/churn candidate from verified evidence-backed observations with code refs, choose a small coherent observation set when the caller does not provide one, favor the strongest selected observation when drafting that candidate, reject or accept a verified candidate, apply explicit map/evidence/churn edits, notify clients after successful writes, and sanity-check those edits before they hit the ledger. It still does not invalidate stale graph regions automatically, execute Epiphany-owned long-running jobs beyond the borrowed runtime `agent_jobs` backend, or coordinate specialist ownership. Teeth are installed; chewing is supervised; the first bell rings; the first wall board exists; the first shift board exists; the first freshness clipboard exists; the first door sensor now exists too; the first binder lens exists; the first pressure gauge exists; the first ember packet exists; the first verdict slip now exists too; the first bounded launch lever and stop switch now exist too; the next slices are about safe waking and real ownership, not jawbones.
In natural language: current Epiphany can preserve a map, show a map, project a compact scene from the map, project derived job/progress slots from the map and retrieval summary, reflect a thin launcher seam over bound work, explicitly launch or interrupt that bound work through a bounded authority surface, emit live bound-job progress notifications from runtime `agent_job_progress` events, project a read-only freshness lens over retrieval and graph staleness, layer watcher-backed invalidation telemetry onto that lens for loaded threads, project a targeted graph/evidence context shard from the map, project context pressure from token telemetry, bank an in-flight investigation packet in durable typed state, reflect that packet back into prompt/scene/context, turn those signals into a read-only resume-versus-regather verdict, explicitly launch one bounded `reorient-specialist` worker from that verdict, retrieve evidence for the map, draft one explicit observation/evidence patch, derive a bounded graph/frontier/churn candidate from verified evidence-backed observations with code refs, choose a small coherent observation set when the caller does not provide one, favor the strongest selected observation when drafting that candidate, reject or accept a verified candidate, apply explicit map/evidence/churn edits, notify clients after successful writes, and sanity-check those edits before they hit the ledger. It still does not invalidate stale graph regions automatically, reflect reorientation-specialist results back into typed state, execute Epiphany-owned long-running jobs beyond the borrowed runtime `agent_jobs` backend, 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 freshness clipboard exists; the first door sensor now exists too; the first binder lens exists; the first pressure gauge exists; the first ember packet exists; the first verdict slip now exists too; the first bounded launch lever and stop switch now exist too; the first explicit wakeup-hand now exists too; the next slices are about safe waking and real ownership, not jawbones.
## Verification Hooks
@ -1357,6 +1363,7 @@ Current tests cover the landed flows at useful seams:
- live app-server smoke for the first Phase 6 context boundary in `tools/epiphany_phase6_context_smoke.py`.
- live app-server smoke for the first Phase 6 pressure boundary in `tools/epiphany_phase6_pressure_smoke.py`.
- live app-server smoke for the first Phase 6 reorientation policy boundary in `tools/epiphany_phase6_reorient_smoke.py`.
- live app-server smoke for the first bounded reorient-launch runtime consumer in `tools/epiphany_phase6_reorient_launch_smoke.py`.
- live app-server smoke for the first Phase 6 job-control boundary in `tools/epiphany_phase6_job_control_smoke.py`.
Useful commands:
@ -1374,5 +1381,6 @@ $env:CARGO_TARGET_DIR='C:\Users\Meta\.cargo-target-codex'; cargo test -p codex-a
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_context_smoke.py'
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_pressure_smoke.py'
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_reorient_smoke.py'
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_reorient_launch_smoke.py'
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_job_control_smoke.py'
```

View file

@ -71,7 +71,9 @@ The landed machine now has:
- live Phase 6 pressure app-server smoke coverage in `tools/epiphany_phase6_pressure_smoke.py`
- durable Phase 6 investigation checkpointing in authoritative typed state, prompt rendering, and scene/context reflection
- read-only Phase 6 CRRC reorientation policy through `thread/epiphany/reorient`
- bounded Phase 6 reorient-guided specialist launch through `thread/epiphany/reorientLaunch`
- live Phase 6 reorientation app-server smoke coverage in `tools/epiphany_phase6_reorient_smoke.py`
- live Phase 6 reorient-launch app-server smoke coverage in `tools/epiphany_phase6_reorient_launch_smoke.py`
- live Phase 6 job-control app-server smoke coverage in `tools/epiphany_phase6_job_control_smoke.py`
The current phase is Phase 6: reflection boundary and observable harness state.
@ -83,12 +85,13 @@ These boundaries are more important than the individual method names:
- `thread/epiphany/retrieve` is read-only.
- `thread/epiphany/distill` is read-only.
- `thread/epiphany/propose` is read-only.
- Durable typed state writes go through `thread/epiphany/update`, accepted `thread/epiphany/promote`, or the bounded `thread/epiphany/jobLaunch` and `thread/epiphany/jobInterrupt` authority surfaces when they mutate `jobBindings`.
- Durable typed state writes go through `thread/epiphany/update`, accepted `thread/epiphany/promote`, or the bounded `thread/epiphany/jobLaunch`, `thread/epiphany/jobInterrupt`, and `thread/epiphany/reorientLaunch` authority surfaces when they mutate `jobBindings`.
- `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 over retrieval summaries plus typed launcher bindings and optional backend snapshots, not a scheduler or durable runtime job store.
- `thread/epiphany/jobsUpdated` is a live notification derived from runtime progress events and launcher-bound job snapshots, not a scheduler, polling daemon, or durable runtime job store.
- `thread/epiphany/jobLaunch` and `thread/epiphany/jobInterrupt` are bounded authority surfaces over durable `jobBindings` plus the current backend adapter, not a hidden scheduler, queue, or second runtime.
- `thread/epiphany/reorientLaunch` is a bounded runtime consumer over the reorientation verdict, not automatic CRRC, a hidden queue, or permission to keep working after drift without an explicit launch.
- `thread/epiphany/freshness` is a derived reflection, not automatic watcher-driven invalidation, a mutation gate, or a hidden refresh scheduler.
- `thread/epiphany/context` is a targeted reflection, not a state writer or hidden proposal engine.
- `thread/epiphany/pressure` is a context-pressure reflection, not an automatic compactor, scheduler, or CRRC coordinator.
@ -129,11 +132,10 @@ and notify typed state. It can.
The next unknowns are:
- how the landed thin launcher seam should expose explicit launch and interrupt authority without becoming a secret scheduler
- how the landed read-only reorientation verdict should be consumed by runtime without turning CRRC into ceremony machinery
- how the launched reorient-specialist results should flow back into typed state or reflection without hiding CRRC policy inside generic runtime job rows
- how the landed watcher-backed invalidation telemetry should be consumed without turning freshness into a secret worker
- how much automatic CRRC coordination belongs in runtime once pressure, freshness, watcher, checkpoint, and reorientation signals all exist
- whether the first bounded CRRC runtime move should reuse the landed `agent_jobs` backend through the explicit job-control seam or wait for a second backend
- whether future bounded CRRC consumers should keep reusing the landed `agent_jobs` backend through the explicit job-control seam or start defining a second backend contract
- what Phase 6 should prove before specialist scheduling begins
## Phase 6 Direction
@ -142,8 +144,8 @@ Phase 6 should grow observable harness state outward from the typed spine.
Useful candidates:
1. Decide how the landed read-only reorientation verdict should be consumed by runtime without turning CRRC into ceremony machinery.
2. Decide how much automatic CRRC coordination belongs now that pressure, freshness, watcher, checkpoint, reorient, and explicit job control all exist.
1. Decide how reorient-launched specialist output should be reflected or distilled back into the harness without auto-promoting or hiding policy inside the runtime backend.
2. Decide how much automatic CRRC coordination belongs now that pressure, freshness, watcher, checkpoint, reorient, explicit job control, and one explicit reorient-launch consumer all exist.
3. Add targeted scene/jobs/context/pressure/reorient 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

View file

@ -28,7 +28,7 @@ Do not trust this file for the exact live HEAD. Always check git.
- Do not copy exact branch or HEAD from this note. Run `git status --short --branch` and `git log --oneline -5`.
- Phase 1 through Phase 5 are complete enough.
- Phase 6 has read-only `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/freshness`, `thread/epiphany/context`, `thread/epiphany/pressure`, and `thread/epiphany/reorient`; durable `jobBindings` now act as a thin Epiphany-owned launcher seam with launcher id, authority scope, and backend kind/job id, `thread/epiphany/jobLaunch` / `thread/epiphany/jobInterrupt` now provide explicit bounded authority over that seam, jobs can overlay it onto live runtime `agent_jobs` progress for loaded threads, and `thread/epiphany/jobsUpdated` now translates live `agent_job_progress:{json}` background events into changed launcher-bound notifications without polling or scheduling. Freshness carries watcher-backed invalidation inputs, and reorient turns checkpoint plus freshness/pressure/watcher signals into a read-only resume-versus-regather verdict.
- Phase 6 has read-only `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/freshness`, `thread/epiphany/context`, `thread/epiphany/pressure`, and `thread/epiphany/reorient`; durable `jobBindings` now act as a thin Epiphany-owned launcher seam with launcher id, authority scope, and backend kind/job id, `thread/epiphany/jobLaunch` / `thread/epiphany/jobInterrupt` now provide explicit bounded authority over that seam, jobs can overlay it onto live runtime `agent_jobs` progress for loaded threads, and `thread/epiphany/jobsUpdated` now translates live `agent_job_progress:{json}` background events into changed launcher-bound notifications without polling or scheduling. Freshness carries watcher-backed invalidation inputs, reorient turns checkpoint plus freshness/pressure/watcher signals into a read-only resume-versus-regather verdict, and `thread/epiphany/reorientLaunch` is now the first explicit runtime consumer over that verdict.
- Durable in-flight investigation checkpointing is now landed in authoritative typed state, writable through `thread/epiphany/update` or accepted `thread/epiphany/promote`, rendered into the prompt, and reflected through scene/context.
- The repo is an Epiphany fork of Codex, not a Codex preset.
- `vendor/codex` is tracked directly, not a submodule.
@ -63,6 +63,7 @@ The current spine:
- verifier-backed promotion through `thread/epiphany/promote`
- successful-write notification through `thread/epiphany/stateUpdated`
- explicit launch/interrupt authority through `thread/epiphany/jobLaunch` and `thread/epiphany/jobInterrupt`
- bounded reorient-guided specialist launch through `thread/epiphany/reorientLaunch`
- thin launcher metadata in durable `jobBindings`
- live bound-job progress notification through `thread/epiphany/jobsUpdated`
- read-only compact reflection through `thread/epiphany/scene`
@ -79,6 +80,7 @@ The current spine:
- live context app-server smoke through `tools/epiphany_phase6_context_smoke.py`
- live pressure app-server smoke through `tools/epiphany_phase6_pressure_smoke.py`
- live reorientation app-server smoke through `tools/epiphany_phase6_reorient_smoke.py`
- live reorient-guided specialist launch smoke through `tools/epiphany_phase6_reorient_launch_smoke.py`
- live job-control app-server smoke through `tools/epiphany_phase6_job_control_smoke.py`
The exact current control flow is documented in
@ -95,7 +97,7 @@ The exact current control flow is documented in
- `thread/epiphany/context` is read-only.
- `thread/epiphany/pressure` is read-only.
- `thread/epiphany/reorient` is read-only.
- Durable typed state writes go through `thread/epiphany/update`, accepted `thread/epiphany/promote`, or the bounded `thread/epiphany/jobLaunch` and `thread/epiphany/jobInterrupt` authority surfaces when they mutate `jobBindings`.
- Durable typed state writes go through `thread/epiphany/update`, accepted `thread/epiphany/promote`, or the bounded `thread/epiphany/jobLaunch`, `thread/epiphany/jobInterrupt`, and `thread/epiphany/reorientLaunch` authority surfaces when they mutate `jobBindings`.
- `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.
- Do not restart Phase 5 hardening without a concrete regression.
@ -156,6 +158,12 @@ For reorientation policy behavior changes, run:
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_reorient_smoke.py'
```
For the bounded reorient-guided specialist launch surface, run:
```powershell
& 'C:\Users\Meta\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' '.\tools\epiphany_phase6_reorient_launch_smoke.py'
```
For Codex Rust work on this Windows machine:
```powershell
@ -220,12 +228,13 @@ still deserves `resume` or has to admit `regather`.
Also keep the guardrail in mind: a read-only reorientation verdict is still not
automatic CRRC. Runtime coordination and next-action execution are still future
work even though read-only job ownership/progress reflection now has a real
runtime seam and a thin launcher boundary.
runtime seam, the thin launcher boundary is landed, and one explicit
reorient-guided launch surface can now consume the verdict on purpose.
When the user asks to continue, the next likely organ is the first bounded
runtime consumer for the landed read-only `thread/epiphany/reorient` verdict,
using the now-explicit job-control seam without turning CRRC into a hidden
scheduler.
read-back path for `thread/epiphany/reorientLaunch` output, so the launched
resume-or-regather specialist does not leave its findings stranded in generic
runtime job rows.
Live `thread/epiphany/scene`, `thread/epiphany/jobs`,
`thread/epiphany/freshness`, `thread/epiphany/context`,

View file

@ -47,3 +47,4 @@
{"ts": "2026-04-26T14:57:54+00:00", "type": "design", "status": "accepted", "note": "Durable jobBindings now define a thin Epiphany-owned launcher seam: launcher identity, authority scope, backend kind, and backend job id belong to Epiphany even while the first backend adapter still targets runtime agent_jobs. This keeps job ownership and authority explicit without prematurely building a second scheduler or runtime."}
{"ts": "2026-04-26T14:57:54+00:00", "type": "verification", "status": "ok", "note": "Phase 6 thin launcher seam landed in durable jobBindings and thread/epiphany/jobs reflection. Jobs and jobsUpdated now surface launcherJobId, authorityScope, backendKind, and backendJobId while the current adapter still resolves runtime agent_jobs progress. Focused codex-core epiphany_state_update tests, app-server-protocol thread_epiphany and schema fixture tests, app-server mapper plus jobsUpdated notification tests, cargo build -p codex-app-server, and tools/epiphany_phase6_jobs_smoke.py passed."}
{"ts": "2026-04-26T15:55:45+00:00", "type": "verification", "status": "ok", "note": "Phase 6 explicit thread/epiphany/jobLaunch and thread/epiphany/jobInterrupt authority landed over the thin launcher seam. The control path now falls back to a direct sqlite state runtime for loaded ephemeral threads, launches or cancels the current agent_jobs backend without turning jobs reflection into a scheduler, and preserves a blocked specialist slot with explicit relaunch reason after interrupt. Focused codex-core epiphany_state_update tests, app-server mapper plus jobsUpdated notification tests, app-server-protocol thread_epiphany and schema fixture checks, cargo build -p codex-app-server, and tools/epiphany_phase6_job_control_smoke.py plus tools/epiphany_phase6_jobs_smoke.py and tools/epiphany_phase6_scene_smoke.py passed."}
{"ts": "2026-04-26T16:30:40+00:00", "type": "verification", "status": "ok", "note": "Phase 6 thread/epiphany/reorientLaunch landed as the first bounded runtime consumer over the read-only reorientation verdict. It launches one fixed reorient-specialist binding through the existing agent_jobs-backed job-control seam with explicit resume/regather scope, checkpoint-derived payload, and no automatic CRRC or hidden scheduler. Focused app-server-protocol thread_epiphany and schema fixture checks, app-server map_epiphany tests, cargo build -p codex-app-server, and tools/epiphany_phase6_scene_smoke.py, tools/epiphany_phase6_reorient_smoke.py, tools/epiphany_phase6_reorient_launch_smoke.py, and tools/epiphany_phase6_job_control_smoke.py passed."}

View file

@ -62,7 +62,7 @@ accepted_design:
- Authoritative Epiphany state lives in Codex protocol/core session and rollout state.
- The prompt reads typed state through a bounded `<epiphany_state>` developer fragment.
- Clients read typed state through `Thread.epiphanyState`, compact reflections such as `thread/epiphany/scene`, `thread/epiphany/jobs`, and `thread/epiphany/freshness`, targeted state shards through `thread/epiphany/context`, runtime pressure through `thread/epiphany/pressure`, bounded CRRC reorientation verdicts through `thread/epiphany/reorient`, and live bound-job progress through `thread/epiphany/jobsUpdated`.
- Durable typed state writes route through `thread/epiphany/update`, accepted `thread/epiphany/promote`, or the bounded `thread/epiphany/jobLaunch` and `thread/epiphany/jobInterrupt` authority surfaces when they mutate `job_bindings`.
- Durable typed state writes route through `thread/epiphany/update`, accepted `thread/epiphany/promote`, or the bounded `thread/epiphany/jobLaunch`, `thread/epiphany/jobInterrupt`, and `thread/epiphany/reorientLaunch` authority surfaces when they mutate `job_bindings`.
- `thread/epiphany/retrieve`, `thread/epiphany/distill`, `thread/epiphany/propose`, `thread/epiphany/scene`, `thread/epiphany/jobs`, `thread/epiphany/freshness`, `thread/epiphany/context`, `thread/epiphany/pressure`, and `thread/epiphany/reorient` stay read-only.
- `thread/epiphany/index` writes the retrieval catalog, not durable Epiphany understanding.
- Qdrant is the preferred persistent semantic backend; BM25 remains the bootstrap/fallback/control path.
@ -80,6 +80,7 @@ accepted_design:
- `thread/epiphany/context` is the first targeted read-only state-shard surface: it selects graph nodes/edges, active frontier, checkpoint, observations, and evidence from authoritative typed state without mutating or notifying.
- `thread/epiphany/pressure` is the first read-only context-pressure surface: it derives pressure from token telemetry and auto-compact/context limits without compacting, scheduling, mutating, or notifying.
- `thread/epiphany/reorient` is the first bounded CRRC policy surface: it derives a read-only `resume` versus `regather` verdict from checkpoint, freshness, watcher, and pressure signals without mutating state, compacting, scheduling, or silently continuing work.
- `thread/epiphany/reorientLaunch` is the first bounded runtime consumer over the reorientation verdict: it launches one fixed `reorient-specialist` worker through the existing job-control seam with explicit resume-versus-regather scope, authority, and checkpoint-derived payload, without making `thread/epiphany/reorient` a writer or turning CRRC into an ambient scheduler.
- Epiphany safety doctrine is to separate cognition from actuation, keep authority explicit, make interruption sacred, stage capability release, and treat operator misuse as a normal design input rather than a rude hypothetical.
- Epiphany is built in the open, but publication mode is not one-size-fits-all: use FOSS where viable, source-available licensing where economics or capability risk demand it, and commercial terms where organizations extract real enterprise value.
- `thread/epiphany/jobLaunch` and `thread/epiphany/jobInterrupt` are the first bounded authority surfaces over the thin job seam: they mutate durable `job_bindings`, create or cancel the current `agent_jobs` backend binding, and preserve explicit launcher identity and interruption without turning `thread/epiphany/jobs` into a scheduler.
@ -97,7 +98,7 @@ landed_baseline:
- Phase 3: typed client read surface through `Thread.epiphanyState`.
- Phase 4: hybrid retrieval plus explicit Qdrant indexing with BM25 fallback.
- Phase 5: typed update/distill/propose/promote/read/notification control plane.
- Phase 6: read-only scene, job/progress, freshness with watcher-backed invalidation inputs, targeted context, context-pressure, and reorientation policy surfaces, plus durable investigation checkpointing for compaction-safe planning, live bound-job progress notifications, a thin launcher seam in durable job bindings, and explicit launch/interrupt authority over that seam.
- Phase 6: read-only scene, job/progress, freshness with watcher-backed invalidation inputs, targeted context, context-pressure, and reorientation policy surfaces, plus durable investigation checkpointing for compaction-safe planning, live bound-job progress notifications, a thin launcher seam in durable job bindings, explicit launch/interrupt authority over that seam, and a bounded reorient-guided specialist launch surface.
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.
@ -108,6 +109,7 @@ landed_baseline:
- Use `tools/epiphany_phase6_context_smoke.py` before modifying targeted context-shard behavior.
- Use `tools/epiphany_phase6_pressure_smoke.py` before modifying context-pressure reflection behavior.
- Use `tools/epiphany_phase6_reorient_smoke.py` before modifying bounded CRRC reorientation policy behavior.
- Use `tools/epiphany_phase6_reorient_launch_smoke.py` before modifying the bounded reorient-guided specialist launch surface.
- Use focused scene/jobs/context/pressure/reorient/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.
@ -126,5 +128,5 @@ rejected_paths:
reason: persistent state that accumulates every local truth becomes memory cruft and weakens rehydration
current_status:
summary: Phase 1 durable state, Phase 2 prompt integration, Phase 3 typed read, Phase 4 retrieval/indexing, Phase 5 update/distill/propose/promote/read/notification, and Phase 6 read-only scene, jobs, freshness, targeted context, context-pressure reflection, watcher-backed invalidation inputs, durable investigation checkpointing, bounded CRRC reorientation policy, live bound-job progress notifications, a thin launcher seam in durable `job_bindings`, and explicit `thread/epiphany/jobLaunch` / `thread/epiphany/jobInterrupt` authority over that seam are landed. `job_bindings` now carry Epiphany-owned launcher metadata such as launcher id, authority scope, and backend kind/job id, while still tolerating the older runtime-agent-job pointer as a compatibility alias. `thread/epiphany/jobs` overlays that launcher seam onto real runtime `agent_jobs` progress for bound work, including runtime/backend job id, worker thread ids, and progress counts, without becoming a scheduler. `thread/epiphany/jobsUpdated` now translates runtime `agent_job_progress:{json}` background events into changed launcher-bound payloads only when the mapped job state actually changes, so clients can watch real owners move without polling the jobs read surface. The launch/interrupt control path now uses direct state-runtime fallback when a loaded ephemeral thread does not already carry a live sqlite handle, so authoritative bindings and runtime snapshots stay aligned instead of pretending the backend vanished. `thread/epiphany/reorient` still consumes checkpoint, freshness, watcher, and pressure signals to return a read-only `resume` versus `regather` verdict with explicit checkpoint-path/frontier reasons, but it does not mutate state, compact, schedule, notify, or silently continue work. Focused core/protocol/app-server tests, schema fixture regeneration/checks, app-server build, live jobs/job-control/scene smokes, and the targeted jobsUpdated notification test cover the seam. Investigation checkpoints still live in authoritative typed state, flow through the prompt, and reflect through scene/context without adding a second writer or fake scheduler. Persistent state was cut down so map, handoff, plan, harness surfaces, scratch, and evidence each carry one job instead of becoming a Jenga tower; evidence is now a distilled ledger rather than an activity feed. The global AGENTS Epiphany core command says persistent state is the agent's mind and explicitly permits the agent to ask for self-modification when memory or workflow makes it worse. The continuity goal is to bank the fire before the dark so compaction becomes less like death and more like sleep.
next_action: After compaction, rehydrate and reorient. If the user asks to continue, the next likely organ is the first bounded runtime consumer for the landed read-only `thread/epiphany/reorient` verdict, using the now-explicit job-control seam without turning CRRC into a hidden scheduler. Automatic runtime CRRC remains future work; do not restart the completed pressure, freshness, watcher-invalidation, investigation-checkpoint, reorientation, jobs-binding, jobsUpdated, thin-launcher-seam, or job-control slice unless a concrete regression appears.
summary: Phase 1 durable state, Phase 2 prompt integration, Phase 3 typed read, Phase 4 retrieval/indexing, Phase 5 update/distill/propose/promote/read/notification, and Phase 6 read-only scene, jobs, freshness, targeted context, context-pressure reflection, watcher-backed invalidation inputs, durable investigation checkpointing, bounded CRRC reorientation policy, live bound-job progress notifications, a thin launcher seam in durable `job_bindings`, explicit `thread/epiphany/jobLaunch` / `thread/epiphany/jobInterrupt` authority over that seam, and a bounded `thread/epiphany/reorientLaunch` runtime consumer are landed. `job_bindings` now carry Epiphany-owned launcher metadata such as launcher id, authority scope, and backend kind/job id, while still tolerating the older runtime-agent-job pointer as a compatibility alias. `thread/epiphany/jobs` overlays that launcher seam onto real runtime `agent_jobs` progress for bound work, including runtime/backend job id, worker thread ids, and progress counts, without becoming a scheduler. `thread/epiphany/jobsUpdated` now translates runtime `agent_job_progress:{json}` background events into changed launcher-bound payloads only when the mapped job state actually changes, so clients can watch real owners move without polling the jobs read surface. The launch/interrupt control path still uses direct state-runtime fallback when a loaded ephemeral thread does not already carry a live sqlite handle, so authoritative bindings and runtime snapshots stay aligned instead of pretending the backend vanished. `thread/epiphany/reorient` still consumes checkpoint, freshness, watcher, and pressure signals to return a read-only `resume` versus `regather` verdict with explicit checkpoint-path/frontier reasons, but it does not mutate state, compact, schedule, notify, or silently continue work. `thread/epiphany/reorientLaunch` is the first explicit runtime consumer of that verdict: it launches one fixed `reorient-specialist` worker with resume-or-regather scope, checkpoint-derived payload, and bounded structured output expectations, without auto-coordinating CRRC or turning the read-only verdict into a writer. Focused protocol/app-server tests, schema fixture regeneration/checks, app-server build, and live scene/reorient/reorient-launch/job-control smokes cover the seam. Investigation checkpoints still live in authoritative typed state, flow through the prompt, and reflect through scene/context without adding a second writer or fake scheduler. Persistent state was cut down so map, handoff, plan, harness surfaces, scratch, and evidence each carry one job instead of becoming a Jenga tower; evidence is now a distilled ledger rather than an activity feed. The global AGENTS Epiphany core command says persistent state is the agent's mind and explicitly permits the agent to ask for self-modification when memory or workflow makes it worse. The continuity goal is to bank the fire before the dark so compaction becomes less like death and more like sleep.
next_action: After compaction, rehydrate and reorient. If the user asks to continue, the next likely organ is the first bounded read-back path for reorientation-specialist output, so `thread/epiphany/reorientLaunch` does not strand its findings inside generic runtime job rows. Automatic runtime CRRC and broad specialist scheduling remain future work; do not restart the completed pressure, freshness, watcher-invalidation, investigation-checkpoint, reorientation, jobs-binding, jobsUpdated, thin-launcher-seam, job-control, or reorient-launch slice unless a concrete regression appears.

View file

@ -0,0 +1,347 @@
from __future__ import annotations
import argparse
import json
from pathlib import Path
import time
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
from epiphany_phase6_reorient_smoke import WATCHED_RELATIVE_PATH
from epiphany_phase6_reorient_smoke import prepare_workspace
from epiphany_phase6_reorient_smoke import reorient_patch
from epiphany_phase6_reorient_smoke import wait_for_regather_reorient
DEFAULT_CODEX_HOME = ROOT / ".epiphany-smoke" / "phase6-reorient-launch-codex-home"
DEFAULT_WORKSPACE = ROOT / ".epiphany-smoke" / "phase6-reorient-launch-workspace"
DEFAULT_RESULT = ROOT / ".epiphany-smoke" / "phase6-reorient-launch-smoke-result.json"
DEFAULT_TRANSCRIPT = ROOT / ".epiphany-smoke" / "phase6-reorient-launch-smoke-transcript.jsonl"
DEFAULT_STDERR = ROOT / ".epiphany-smoke" / "phase6-reorient-launch-smoke-server.stderr.log"
BINDING_ID = "reorient-specialist"
SUBGOAL_ID = "phase6-reorient-smoke"
GRAPH_NODE_ID = "reorient-target"
def job_by_id(jobs: list[dict[str, Any]], job_id: str) -> dict[str, Any]:
for job in jobs:
if job["id"] == job_id:
return job
raise AssertionError(f"missing job {job_id!r}: {jobs!r}")
def assert_reorient_job(
job: dict[str, Any],
*,
action: str,
) -> None:
require(job["id"] == BINDING_ID, "reorient launch should use the fixed binding id")
require(job["kind"] == "specialist", "reorient launch should use a specialist job")
require(job["ownerRole"] == "epiphany-reorient", "reorient launch should use the fixed owner role")
require(
job["authorityScope"] == f"epiphany.reorient.{action}",
"reorient launch should expose the fixed authority scope",
)
require(
job["scope"] == f"reorient-guided checkpoint {action}",
"reorient launch should expose the reorientation scope",
)
require(
isinstance(job.get("launcherJobId"), str)
and job["launcherJobId"].startswith("epiphany-launch-"),
"reorient launch should expose an Epiphany launcher id",
)
require(
job["backendKind"] == "agentJobs",
"reorient launch should resolve through the agent_jobs backend",
)
require(
isinstance(job.get("backendJobId"), str) and job["backendJobId"],
"reorient launch should expose a backend job id",
)
require(
job["runtimeAgentJobId"] == job["backendJobId"],
"reorient launch should keep runtime and backend job ids aligned",
)
require(
job["linkedSubgoalIds"] == [SUBGOAL_ID],
"reorient launch should bind the active subgoal",
)
require(
job["linkedGraphNodeIds"] == [GRAPH_NODE_ID],
"reorient launch should bind the active frontier node",
)
require(
job["status"] in {"pending", "running", "completed", "failed", "cancelled"},
"reorient launch should bind to a real backend status",
)
def wait_for_jobs_surface(
client: AppServerClient,
thread_id: str,
*,
require_bound_backend: bool,
timeout: float = 20.0,
) -> dict[str, Any]:
deadline = time.time() + timeout
while time.time() < deadline:
response = client.send("thread/epiphany/jobs", {"threadId": thread_id})
assert response is not None
job = job_by_id(response["jobs"], BINDING_ID)
if require_bound_backend and job.get("backendJobId"):
return response
if not require_bound_backend and job.get("backendJobId") is None:
return response
time.sleep(0.2)
raise TimeoutError("timed out waiting for expected Epiphany jobs surface state")
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()
workspace = args.workspace.resolve()
watched_file = prepare_workspace(workspace)
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-reorient-launch-smoke",
"title": "Epiphany Phase 6 Reorient Launch Smoke",
"version": "0.1.0",
},
"capabilities": {"experimentalApi": True},
},
)
client.send("initialized", expect_response=False)
started = client.send("thread/start", {"cwd": str(workspace), "ephemeral": True})
assert started is not None
thread_id = started["thread"]["id"]
update_notification_start = len(client.notifications)
update = client.send(
"thread/epiphany/update",
{"threadId": thread_id, "expectedRevision": 0, "patch": reorient_patch()},
)
assert update is not None
require(update["revision"] == 1, "reorient launch smoke patch should advance revision to 1")
client.wait_for_notification(
"thread/epiphany/stateUpdated",
start_index=update_notification_start,
)
resume_launch_notification_start = len(client.notifications)
resume_launch = client.send(
"thread/epiphany/reorientLaunch",
{
"threadId": thread_id,
"expectedRevision": 1,
"maxRuntimeSeconds": 90,
},
)
assert resume_launch is not None
require(resume_launch["source"] == "live", "reorient launch should consume live state")
require(resume_launch["stateStatus"] == "ready", "reorient launch should require ready state")
require(
resume_launch["stateRevision"] == 1,
"reorient launch should report the consumed checkpoint revision",
)
require(
resume_launch["decision"]["action"] == "resume",
"clean checkpoint should launch a resume-guided specialist",
)
require(
resume_launch["decision"]["reasons"] == ["checkpointReady"],
"resume launch should preserve the checkpoint-ready verdict",
)
require(
resume_launch["revision"] == 2,
"resume launch should advance the durable revision",
)
require(
resume_launch["changedFields"] == ["jobBindings"],
"resume launch should only mutate job bindings",
)
require(
resume_launch["epiphanyState"]["revision"] == 2,
"resume launch should return the persisted revision",
)
assert_reorient_job(resume_launch["job"], action="resume")
resume_launch_notification = client.wait_for_notification(
"thread/epiphany/stateUpdated",
start_index=resume_launch_notification_start,
timeout=15.0,
)
require(
resume_launch_notification["params"]["source"] == "jobLaunch",
"reorient launch should emit the bounded jobLaunch state update source",
)
require(
resume_launch_notification["params"]["revision"] == 2,
"resume launch notification should expose revision 2",
)
require(
resume_launch_notification["params"]["changedFields"] == ["jobBindings"],
"resume launch notification should identify the job-binding mutation",
)
resume_jobs = wait_for_jobs_surface(client, thread_id, require_bound_backend=True)
require(
resume_jobs["stateRevision"] == 2,
"jobs surface should reflect the launched resume revision",
)
assert_reorient_job(job_by_id(resume_jobs["jobs"], BINDING_ID), action="resume")
watched_file.write_text(
"pub fn reorient_target() -> &'static str {\n \"after\"\n}\n",
encoding="utf-8",
)
regather_reorient = wait_for_regather_reorient(client, thread_id)
require(
regather_reorient["decision"]["action"] == "regather",
"touched checkpoint path should force a regather verdict",
)
interrupt_notification_start = len(client.notifications)
interrupt = client.send(
"thread/epiphany/jobInterrupt",
{
"threadId": thread_id,
"expectedRevision": 2,
"bindingId": BINDING_ID,
"reason": "Reorient launch smoke is clearing the resume binding before relaunching in regather mode.",
},
)
assert interrupt is not None
require(interrupt["revision"] == 3, "interrupt should advance the durable revision")
client.wait_for_notification(
"thread/epiphany/stateUpdated",
start_index=interrupt_notification_start,
timeout=15.0,
)
interrupted_jobs = wait_for_jobs_surface(client, thread_id, require_bound_backend=False)
require(
interrupted_jobs["stateRevision"] == 3,
"jobs surface should reflect the interrupted revision",
)
regather_launch_notification_start = len(client.notifications)
regather_launch = client.send(
"thread/epiphany/reorientLaunch",
{
"threadId": thread_id,
"expectedRevision": 3,
"maxRuntimeSeconds": 90,
},
)
assert regather_launch is not None
require(
regather_launch["stateRevision"] == 3,
"regather launch should report the consumed interrupted revision",
)
require(
regather_launch["decision"]["action"] == "regather",
"invalidated checkpoint should launch a regather-guided specialist",
)
require(
regather_launch["decision"]["reasons"] == ["checkpointPathsChanged", "frontierChanged"],
"regather launch should preserve the watcher invalidation verdict",
)
require(
regather_launch["revision"] == 4,
"regather launch should advance the durable revision again",
)
assert_reorient_job(regather_launch["job"], action="regather")
normalized_changed_paths = [
path.replace("\\", "/")
for path in regather_launch["decision"]["checkpointChangedPaths"]
]
require(
WATCHED_RELATIVE_PATH.as_posix() in normalized_changed_paths,
"regather launch should preserve the touched checkpoint path in its decision",
)
regather_launch_notification = client.wait_for_notification(
"thread/epiphany/stateUpdated",
start_index=regather_launch_notification_start,
timeout=15.0,
)
require(
regather_launch_notification["params"]["source"] == "jobLaunch",
"regather launch should emit the bounded jobLaunch state update source",
)
require(
regather_launch_notification["params"]["revision"] == 4,
"regather launch notification should expose revision 4",
)
regather_jobs = wait_for_jobs_surface(client, thread_id, require_bound_backend=True)
require(
regather_jobs["stateRevision"] == 4,
"jobs surface should reflect the relaunched regather revision",
)
assert_reorient_job(job_by_id(regather_jobs["jobs"], BINDING_ID), action="regather")
final_read = client.send("thread/read", {"threadId": thread_id, "includeTurns": False})
assert final_read is not None
require(
final_read["thread"]["epiphanyState"]["revision"] == 4,
"reorient launch smoke should leave the final revision at 4",
)
result = {
"threadId": thread_id,
"codexHome": str(codex_home),
"workspace": str(workspace),
"resumeAction": resume_launch["decision"]["action"],
"resumeRevision": resume_launch["revision"],
"resumeScope": resume_launch["job"]["scope"],
"interruptRevision": interrupt["revision"],
"regatherAction": regather_launch["decision"]["action"],
"regatherRevision": regather_launch["revision"],
"regatherScope": regather_launch["job"]["scope"],
"checkpointChangedPaths": regather_launch["decision"]["checkpointChangedPaths"],
"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 bounded Phase 6 reorient-guided specialist launch 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("--workspace", type=Path, default=DEFAULT_WORKSPACE)
parser.add_argument("--result", type=Path, default=DEFAULT_RESULT)
parser.add_argument("--transcript", type=Path, default=DEFAULT_TRANSCRIPT)
parser.add_argument("--stderr", type=Path, default=DEFAULT_STDERR)
args = parser.parse_args()
result = run_smoke(args)
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -244,6 +244,7 @@ def assert_ready_scene(scene: dict[str, Any], expected_revision: int) -> None:
"freshness",
"pressure",
"reorient",
"reorientLaunch",
"update",
"jobInterrupt",
"propose",

View file

@ -15985,6 +15985,7 @@
"freshness",
"pressure",
"reorient",
"reorientLaunch",
"propose",
"promote",
"update"

View file

@ -13872,6 +13872,7 @@
"freshness",
"pressure",
"reorient",
"reorientLaunch",
"propose",
"promote",
"update"

View file

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

View file

@ -366,6 +366,11 @@ client_request_definitions! {
params: v2::ThreadEpiphanyReorientParams,
response: v2::ThreadEpiphanyReorientResponse,
},
#[experimental("thread/epiphany/reorientLaunch")]
ThreadEpiphanyReorientLaunch => "thread/epiphany/reorientLaunch" {
params: v2::ThreadEpiphanyReorientLaunchParams,
response: v2::ThreadEpiphanyReorientLaunchResponse,
},
#[experimental("thread/epiphany/index")]
ThreadEpiphanyIndex => "thread/epiphany/index" {
params: v2::ThreadEpiphanyIndexParams,
@ -1710,6 +1715,7 @@ mod tests {
v2::ThreadEpiphanySceneAction::Freshness,
v2::ThreadEpiphanySceneAction::Pressure,
v2::ThreadEpiphanySceneAction::Reorient,
v2::ThreadEpiphanySceneAction::ReorientLaunch,
v2::ThreadEpiphanySceneAction::Update,
v2::ThreadEpiphanySceneAction::JobInterrupt,
v2::ThreadEpiphanySceneAction::Propose,
@ -1809,6 +1815,7 @@ mod tests {
"freshness",
"pressure",
"reorient",
"reorientLaunch",
"update",
"jobInterrupt",
"propose",
@ -2225,6 +2232,156 @@ mod tests {
Ok(())
}
#[test]
fn serialize_thread_epiphany_reorient_launch_response() -> Result<()> {
let response = ClientResponse::ThreadEpiphanyReorientLaunch {
request_id: RequestId::Integer(8),
response: v2::ThreadEpiphanyReorientLaunchResponse {
thread_id: "thr_123".to_string(),
source: v2::ThreadEpiphanyReorientSource::Live,
state_status: v2::ThreadEpiphanyReorientStateStatus::Ready,
state_revision: Some(4),
decision: v2::ThreadEpiphanyReorientDecision {
action: v2::ThreadEpiphanyReorientAction::Regather,
checkpoint_status: v2::ThreadEpiphanyReorientCheckpointStatus::ResumeReady,
checkpoint_id: Some("ix-1".to_string()),
pressure_level: v2::ThreadEpiphanyPressureLevel::High,
retrieval_status: v2::ThreadEpiphanyRetrievalFreshnessStatus::Stale,
graph_status: v2::ThreadEpiphanyGraphFreshnessStatus::Ready,
watcher_status: v2::ThreadEpiphanyInvalidationStatus::Changed,
reasons: vec![
v2::ThreadEpiphanyReorientReason::CheckpointPathsChanged,
v2::ThreadEpiphanyReorientReason::FrontierChanged,
],
checkpoint_dirty_paths: vec![PathBuf::from("src/lib.rs")],
checkpoint_changed_paths: vec![PathBuf::from("src/lib.rs")],
active_frontier_node_ids: vec!["state-spine".to_string()],
next_action: "Re-gather source before editing.".to_string(),
note: "Resume-ready checkpoint was invalidated by watcher/frontier drift."
.to_string(),
},
revision: 5,
changed_fields: vec![v2::ThreadEpiphanyStateUpdatedField::JobBindings],
epiphany_state: codex_protocol::protocol::EpiphanyThreadState {
revision: 5,
job_bindings: vec![codex_protocol::protocol::EpiphanyJobBinding {
id: "reorient-specialist".to_string(),
kind: codex_protocol::protocol::EpiphanyJobKind::Specialist,
scope: "reorient-guided checkpoint regather".to_string(),
owner_role: "epiphany-reorient".to_string(),
launcher_job_id: Some("epiphany-launch-1".to_string()),
authority_scope: Some("epiphany.reorient.regather".to_string()),
backend_kind: Some(
codex_protocol::protocol::EpiphanyJobBackendKind::AgentJobs,
),
backend_job_id: Some("backend-1".to_string()),
runtime_agent_job_id: Some("backend-1".to_string()),
linked_subgoal_ids: vec!["phase-6".to_string()],
linked_graph_node_ids: vec!["state-spine".to_string()],
progress_note: Some(
"Explicitly launched through the Epiphany authority surface onto the agent_jobs backend."
.to_string(),
),
blocking_reason: None,
}],
..Default::default()
},
job: v2::ThreadEpiphanyJob {
id: "reorient-specialist".to_string(),
kind: v2::ThreadEpiphanyJobKind::Specialist,
scope: "reorient-guided checkpoint regather".to_string(),
owner_role: "epiphany-reorient".to_string(),
launcher_job_id: Some("epiphany-launch-1".to_string()),
authority_scope: Some("epiphany.reorient.regather".to_string()),
backend_kind: Some(v2::ThreadEpiphanyJobBackendKind::AgentJobs),
backend_job_id: Some("backend-1".to_string()),
status: v2::ThreadEpiphanyJobStatus::Pending,
runtime_agent_job_id: Some("backend-1".to_string()),
items_processed: None,
items_total: None,
progress_note: Some(
"Explicitly launched through the Epiphany authority surface onto the agent_jobs backend."
.to_string(),
),
last_checkpoint_at_unix_seconds: None,
blocking_reason: None,
active_thread_ids: Vec::new(),
linked_subgoal_ids: vec!["phase-6".to_string()],
linked_graph_node_ids: vec!["state-spine".to_string()],
},
},
};
assert_eq!(response.id(), &RequestId::Integer(8));
assert_eq!(response.method(), "thread/epiphany/reorientLaunch");
assert_eq!(
json!({
"method": "thread/epiphany/reorientLaunch",
"id": 8,
"response": {
"threadId": "thr_123",
"source": "live",
"stateStatus": "ready",
"stateRevision": 4,
"decision": {
"action": "regather",
"checkpointStatus": "resumeReady",
"checkpointId": "ix-1",
"pressureLevel": "high",
"retrievalStatus": "stale",
"graphStatus": "ready",
"watcherStatus": "changed",
"reasons": [
"checkpointPathsChanged",
"frontierChanged"
],
"checkpointDirtyPaths": ["src/lib.rs"],
"checkpointChangedPaths": ["src/lib.rs"],
"activeFrontierNodeIds": ["state-spine"],
"nextAction": "Re-gather source before editing.",
"note": "Resume-ready checkpoint was invalidated by watcher/frontier drift."
},
"revision": 5,
"changedFields": ["jobBindings"],
"epiphanyState": {
"revision": 5,
"job_bindings": [{
"id": "reorient-specialist",
"kind": "specialist",
"scope": "reorient-guided checkpoint regather",
"owner_role": "epiphany-reorient",
"launcher_job_id": "epiphany-launch-1",
"authority_scope": "epiphany.reorient.regather",
"backend_kind": "agent_jobs",
"backend_job_id": "backend-1",
"runtime_agent_job_id": "backend-1",
"linked_subgoal_ids": ["phase-6"],
"linked_graph_node_ids": ["state-spine"],
"progress_note": "Explicitly launched through the Epiphany authority surface onto the agent_jobs backend."
}]
},
"job": {
"id": "reorient-specialist",
"kind": "specialist",
"scope": "reorient-guided checkpoint regather",
"ownerRole": "epiphany-reorient",
"launcherJobId": "epiphany-launch-1",
"authorityScope": "epiphany.reorient.regather",
"backendKind": "agentJobs",
"backendJobId": "backend-1",
"status": "pending",
"runtimeAgentJobId": "backend-1",
"progressNote": "Explicitly launched through the Epiphany authority surface onto the agent_jobs backend.",
"linkedSubgoalIds": ["phase-6"],
"linkedGraphNodeIds": ["state-spine"]
}
}
}),
serde_json::to_value(&response)?,
);
Ok(())
}
#[test]
fn serialize_thread_epiphany_retrieve_response() -> Result<()> {
let response = ClientResponse::ThreadEpiphanyRetrieve {
@ -3443,6 +3600,20 @@ mod tests {
assert_eq!(reason, Some("thread/epiphany/reorient"));
}
#[test]
fn thread_epiphany_reorient_launch_is_marked_experimental() {
let request = ClientRequest::ThreadEpiphanyReorientLaunch {
request_id: RequestId::Integer(1),
params: v2::ThreadEpiphanyReorientLaunchParams {
thread_id: "thr_123".to_string(),
expected_revision: Some(4),
max_runtime_seconds: Some(90),
},
};
let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request);
assert_eq!(reason, Some("thread/epiphany/reorientLaunch"));
}
#[test]
fn thread_epiphany_index_is_marked_experimental() {
let request = ClientRequest::ThreadEpiphanyIndex {

View file

@ -4002,6 +4002,7 @@ pub enum ThreadEpiphanySceneAction {
Freshness,
Pressure,
Reorient,
ReorientLaunch,
Propose,
Promote,
Update,
@ -4560,6 +4561,36 @@ pub struct ThreadEpiphanyReorientResponse {
pub decision: ThreadEpiphanyReorientDecision,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadEpiphanyReorientLaunchParams {
pub thread_id: String,
#[serde(default)]
#[ts(optional = nullable)]
pub expected_revision: Option<u64>,
#[serde(default)]
#[ts(optional = nullable)]
pub max_runtime_seconds: Option<u64>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadEpiphanyReorientLaunchResponse {
pub thread_id: String,
pub source: ThreadEpiphanyReorientSource,
pub state_status: ThreadEpiphanyReorientStateStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub state_revision: Option<u64>,
pub decision: ThreadEpiphanyReorientDecision,
pub revision: u64,
pub changed_fields: Vec<ThreadEpiphanyStateUpdatedField>,
pub epiphany_state: CoreEpiphanyThreadState,
pub job: ThreadEpiphanyJob,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]

View file

@ -193,6 +193,8 @@ use codex_app_server_protocol::ThreadEpiphanyProposeResponse;
use codex_app_server_protocol::ThreadEpiphanyReorientAction;
use codex_app_server_protocol::ThreadEpiphanyReorientCheckpointStatus;
use codex_app_server_protocol::ThreadEpiphanyReorientDecision;
use codex_app_server_protocol::ThreadEpiphanyReorientLaunchParams;
use codex_app_server_protocol::ThreadEpiphanyReorientLaunchResponse;
use codex_app_server_protocol::ThreadEpiphanyReorientParams;
use codex_app_server_protocol::ThreadEpiphanyReorientReason;
use codex_app_server_protocol::ThreadEpiphanyReorientResponse;
@ -1085,6 +1087,10 @@ impl CodexMessageProcessor {
self.thread_epiphany_reorient(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::ThreadEpiphanyReorientLaunch { request_id, params } => {
self.thread_epiphany_reorient_launch(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;
@ -4510,6 +4516,166 @@ impl CodexMessageProcessor {
self.outgoing.send_response(request_id, response).await;
}
async fn thread_epiphany_reorient_launch(
&self,
request_id: ConnectionRequestId,
params: ThreadEpiphanyReorientLaunchParams,
) {
let ThreadEpiphanyReorientLaunchParams {
thread_id,
expected_revision,
max_runtime_seconds,
} = params;
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_thread = match self.thread_manager.get_thread(thread_uuid).await {
Ok(thread) => thread,
Err(_) => {
self.send_invalid_request_error(
request_id,
format!("thread not loaded: {thread_uuid}"),
)
.await;
return;
}
};
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 retrieval_override = loaded_thread.epiphany_retrieval_state().await;
let config_snapshot = loaded_thread.config_snapshot().await;
self.epiphany_invalidation_manager
.ensure_thread_watch(&thread_id, &config_snapshot.cwd)
.await;
let watcher_snapshot = self
.epiphany_invalidation_manager
.snapshot(&thread_id)
.await;
let token_usage_info = loaded_thread.token_usage_info().await;
let (state_revision, retrieval, graph, watcher) = map_epiphany_freshness(
thread.epiphany_state.as_ref(),
Some(&retrieval_override),
Some(&watcher_snapshot),
);
let pressure = map_epiphany_pressure(token_usage_info.as_ref());
let (state_status, decision) = map_epiphany_reorient(
thread.epiphany_state.as_ref(),
&pressure,
&retrieval,
&graph,
&watcher,
);
let Some(state) = thread.epiphany_state.as_ref() else {
self.send_invalid_request_error(
request_id,
format!(
"cannot launch a reorientation specialist without authoritative Epiphany state: {}",
decision.note
),
)
.await;
return;
};
let Some(checkpoint) = state.investigation_checkpoint.as_ref() else {
self.send_invalid_request_error(
request_id,
format!(
"cannot launch a reorientation specialist without a durable investigation checkpoint: {}",
decision.note
),
)
.await;
return;
};
let launch_request = build_epiphany_reorient_launch_request(
&thread_id,
expected_revision,
max_runtime_seconds,
state,
checkpoint,
&decision,
);
let changed_fields = vec![ThreadEpiphanyStateUpdatedField::JobBindings];
let launched = match loaded_thread.epiphany_launch_job(launch_request).await {
Ok(launched) => launched,
Err(CodexErr::InvalidRequest(message)) => {
self.send_invalid_request_error(request_id, message).await;
return;
}
Err(err) => {
self.send_internal_error(
request_id,
format!(
"failed to launch Epiphany reorientation specialist for {thread_uuid}: {err}"
),
)
.await;
return;
}
};
let epiphany_state = client_visible_live_thread_epiphany_state(
loaded_thread.as_ref(),
launched.epiphany_state,
)
.await;
let state_db_ctx = loaded_thread.epiphany_state_runtime().await;
let job_resolution =
load_epiphany_job_launcher_snapshots(state_db_ctx.as_ref(), Some(&epiphany_state))
.await;
let job = map_epiphany_jobs(Some(&epiphany_state), None, &job_resolution)
.into_iter()
.find(|job| job.id == EPIPHANY_REORIENT_LAUNCH_BINDING_ID)
.unwrap_or_else(map_epiphany_specialist_job);
self.outgoing
.send_response(
request_id,
ThreadEpiphanyReorientLaunchResponse {
thread_id: thread_uuid.to_string(),
source: ThreadEpiphanyReorientSource::Live,
state_status,
state_revision,
decision,
revision: epiphany_state.revision,
changed_fields: changed_fields.clone(),
epiphany_state: epiphany_state.clone(),
job,
},
)
.await;
self.outgoing
.send_server_notification(ServerNotification::ThreadEpiphanyStateUpdated(
ThreadEpiphanyStateUpdatedNotification {
thread_id: thread_uuid.to_string(),
source: ThreadEpiphanyStateUpdatedSource::JobLaunch,
revision: epiphany_state.revision,
changed_fields,
epiphany_state,
},
))
.await;
}
async fn thread_epiphany_retrieve(
&self,
request_id: ConnectionRequestId,
@ -11159,7 +11325,13 @@ fn map_epiphany_scene(state: Option<&EpiphanyThreadState>, loaded: bool) -> Thre
} else {
ThreadEpiphanySceneSource::Stored
};
let available_actions = epiphany_scene_available_actions(loaded, state.is_some());
let available_actions = epiphany_scene_available_actions(
loaded,
state.is_some(),
state
.and_then(|state| state.investigation_checkpoint.as_ref())
.is_some(),
);
let Some(state) = state else {
return ThreadEpiphanyScene {
state_status: ThreadEpiphanySceneStateStatus::Missing,
@ -11262,10 +11434,13 @@ fn map_epiphany_scene(state: Option<&EpiphanyThreadState>, loaded: bool) -> Thre
}
const EPIPHANY_SCENE_RECORD_LIMIT: usize = 5;
const EPIPHANY_REORIENT_LAUNCH_BINDING_ID: &str = "reorient-specialist";
const EPIPHANY_REORIENT_OWNER_ROLE: &str = "epiphany-reorient";
fn epiphany_scene_available_actions(
loaded: bool,
state_present: bool,
checkpoint_present: bool,
) -> Vec<ThreadEpiphanySceneAction> {
if !loaded {
return Vec::new();
@ -11281,8 +11456,11 @@ fn epiphany_scene_available_actions(
ThreadEpiphanySceneAction::Freshness,
ThreadEpiphanySceneAction::Pressure,
ThreadEpiphanySceneAction::Reorient,
ThreadEpiphanySceneAction::Update,
];
if checkpoint_present {
actions.push(ThreadEpiphanySceneAction::ReorientLaunch);
}
actions.push(ThreadEpiphanySceneAction::Update);
if state_present {
actions.push(ThreadEpiphanySceneAction::JobInterrupt);
actions.push(ThreadEpiphanySceneAction::Propose);
@ -11291,6 +11469,213 @@ fn epiphany_scene_available_actions(
actions
}
fn build_epiphany_reorient_launch_request(
thread_id: &str,
expected_revision: Option<u64>,
max_runtime_seconds: Option<u64>,
state: &EpiphanyThreadState,
checkpoint: &codex_protocol::protocol::EpiphanyInvestigationCheckpoint,
decision: &ThreadEpiphanyReorientDecision,
) -> EpiphanyJobLaunchRequest {
let linked_subgoal_ids = epiphany_active_subgoal_ids(Some(state));
let linked_graph_node_ids = unique_strings(
epiphany_active_graph_node_ids(Some(state))
.into_iter()
.chain(decision.active_frontier_node_ids.iter().cloned()),
);
let checkpoint_next_action = checkpoint
.next_action
.clone()
.unwrap_or_else(|| decision.next_action.clone());
let authority_scope = match decision.action {
ThreadEpiphanyReorientAction::Resume => "epiphany.reorient.resume",
ThreadEpiphanyReorientAction::Regather => "epiphany.reorient.regather",
};
let scope = match decision.action {
ThreadEpiphanyReorientAction::Resume => "reorient-guided checkpoint resume",
ThreadEpiphanyReorientAction::Regather => "reorient-guided checkpoint regather",
};
let instruction = build_epiphany_reorient_launch_instruction(decision.action);
let input_json = serde_json::json!({
"threadId": thread_id,
"mode": reorient_action_label(decision.action),
"checkpointId": checkpoint.checkpoint_id.clone(),
"checkpointKind": checkpoint.kind.clone(),
"checkpointDisposition": investigation_disposition_label(checkpoint.disposition),
"checkpointFocus": checkpoint.focus.clone(),
"checkpointSummary": checkpoint.summary.clone(),
"checkpointNextAction": checkpoint_next_action,
"checkpointOpenQuestions": checkpoint.open_questions.clone(),
"checkpointEvidenceIds": checkpoint.evidence_ids.clone(),
"checkpointCodeRefs": checkpoint
.code_refs
.iter()
.map(epiphany_code_ref_json)
.collect::<Vec<_>>(),
"decisionReasons": decision
.reasons
.iter()
.map(|reason| reorient_reason_label(*reason))
.collect::<Vec<_>>(),
"decisionNote": decision.note.clone(),
"pressureLevel": pressure_level_label(decision.pressure_level),
"retrievalStatus": retrieval_freshness_status_label(decision.retrieval_status),
"graphStatus": graph_freshness_status_label(decision.graph_status),
"watcherStatus": invalidation_status_label(decision.watcher_status),
"checkpointDirtyPaths": decision
.checkpoint_dirty_paths
.iter()
.map(path_to_display_string)
.collect::<Vec<_>>(),
"checkpointChangedPaths": decision
.checkpoint_changed_paths
.iter()
.map(path_to_display_string)
.collect::<Vec<_>>(),
"activeFrontierNodeIds": decision.active_frontier_node_ids.clone(),
"linkedSubgoalIds": linked_subgoal_ids,
"linkedGraphNodeIds": linked_graph_node_ids,
});
EpiphanyJobLaunchRequest {
expected_revision,
binding_id: EPIPHANY_REORIENT_LAUNCH_BINDING_ID.to_string(),
kind: CoreEpiphanyJobKind::Specialist,
scope: scope.to_string(),
owner_role: EPIPHANY_REORIENT_OWNER_ROLE.to_string(),
authority_scope: authority_scope.to_string(),
linked_subgoal_ids: epiphany_active_subgoal_ids(Some(state)),
linked_graph_node_ids: unique_strings(
epiphany_active_graph_node_ids(Some(state))
.into_iter()
.chain(decision.active_frontier_node_ids.iter().cloned()),
),
instruction,
input_json,
output_schema_json: Some(epiphany_reorient_launch_output_schema()),
max_runtime_seconds,
}
}
fn build_epiphany_reorient_launch_instruction(action: ThreadEpiphanyReorientAction) -> String {
match action {
ThreadEpiphanyReorientAction::Resume => "Use the durable Epiphany investigation checkpoint to resume one bounded source-grounding pass. Verify that the checkpointed seam still makes sense, answer only the local open questions you can ground from source, and return a compact structured result. Keep scope tight. Do not drift into broad implementation or invent evidence you did not actually inspect.".to_string(),
ThreadEpiphanyReorientAction::Regather => "Treat the durable Epiphany investigation checkpoint as invalidated and re-gather the seam before any implementation. Inspect the changed checkpoint paths and frontier hits, explain what broke the old packet, and return a compact structured result with the next safe move. Do not continue broad implementation until the seam is re-grounded.".to_string(),
}
}
fn epiphany_reorient_launch_output_schema() -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": ["resume", "regather"]
},
"summary": {"type": "string"},
"nextSafeMove": {"type": "string"},
"checkpointStillValid": {"type": "boolean"},
"filesInspected": {
"type": "array",
"items": {"type": "string"}
},
"frontierNodeIds": {
"type": "array",
"items": {"type": "string"}
},
"evidenceIds": {
"type": "array",
"items": {"type": "string"}
}
},
"required": ["mode", "summary", "nextSafeMove"],
"additionalProperties": true
})
}
fn epiphany_code_ref_json(
code_ref: &codex_protocol::protocol::EpiphanyCodeRef,
) -> serde_json::Value {
serde_json::json!({
"path": path_to_display_string(code_ref.path.as_path()),
"startLine": code_ref.start_line,
"endLine": code_ref.end_line,
"symbol": code_ref.symbol,
"note": code_ref.note,
})
}
fn path_to_display_string(path: impl AsRef<Path>) -> String {
path.as_ref().to_string_lossy().into_owned()
}
fn reorient_action_label(action: ThreadEpiphanyReorientAction) -> &'static str {
match action {
ThreadEpiphanyReorientAction::Resume => "resume",
ThreadEpiphanyReorientAction::Regather => "regather",
}
}
fn investigation_disposition_label(disposition: EpiphanyInvestigationDisposition) -> &'static str {
match disposition {
EpiphanyInvestigationDisposition::ResumeReady => "resume_ready",
EpiphanyInvestigationDisposition::RegatherRequired => "regather_required",
}
}
fn reorient_reason_label(reason: ThreadEpiphanyReorientReason) -> &'static str {
match reason {
ThreadEpiphanyReorientReason::MissingState => "missingState",
ThreadEpiphanyReorientReason::MissingCheckpoint => "missingCheckpoint",
ThreadEpiphanyReorientReason::CheckpointRequestedRegather => "checkpointRequestedRegather",
ThreadEpiphanyReorientReason::CheckpointPathsDirty => "checkpointPathsDirty",
ThreadEpiphanyReorientReason::CheckpointPathsChanged => "checkpointPathsChanged",
ThreadEpiphanyReorientReason::FrontierChanged => "frontierChanged",
ThreadEpiphanyReorientReason::UnanchoredCheckpointWhileStateStale => {
"unanchoredCheckpointWhileStateStale"
}
ThreadEpiphanyReorientReason::CheckpointReady => "checkpointReady",
}
}
fn pressure_level_label(level: ThreadEpiphanyPressureLevel) -> &'static str {
match level {
ThreadEpiphanyPressureLevel::Unknown => "unknown",
ThreadEpiphanyPressureLevel::Low => "low",
ThreadEpiphanyPressureLevel::Elevated => "elevated",
ThreadEpiphanyPressureLevel::High => "high",
ThreadEpiphanyPressureLevel::Critical => "critical",
}
}
fn retrieval_freshness_status_label(
status: ThreadEpiphanyRetrievalFreshnessStatus,
) -> &'static str {
match status {
ThreadEpiphanyRetrievalFreshnessStatus::Missing => "missing",
ThreadEpiphanyRetrievalFreshnessStatus::Ready => "ready",
ThreadEpiphanyRetrievalFreshnessStatus::Stale => "stale",
ThreadEpiphanyRetrievalFreshnessStatus::Indexing => "indexing",
ThreadEpiphanyRetrievalFreshnessStatus::Unavailable => "unavailable",
}
}
fn graph_freshness_status_label(status: ThreadEpiphanyGraphFreshnessStatus) -> &'static str {
match status {
ThreadEpiphanyGraphFreshnessStatus::Missing => "missing",
ThreadEpiphanyGraphFreshnessStatus::Ready => "ready",
ThreadEpiphanyGraphFreshnessStatus::Stale => "stale",
}
}
fn invalidation_status_label(status: ThreadEpiphanyInvalidationStatus) -> &'static str {
match status {
ThreadEpiphanyInvalidationStatus::Unavailable => "unavailable",
ThreadEpiphanyInvalidationStatus::Clean => "clean",
ThreadEpiphanyInvalidationStatus::Changed => "changed",
}
}
fn map_epiphany_freshness(
state: Option<&EpiphanyThreadState>,
retrieval_override: Option<&EpiphanyRetrievalState>,
@ -14378,6 +14763,7 @@ mod tests {
ThreadEpiphanySceneAction::Freshness,
ThreadEpiphanySceneAction::Pressure,
ThreadEpiphanySceneAction::Reorient,
ThreadEpiphanySceneAction::ReorientLaunch,
ThreadEpiphanySceneAction::Update,
ThreadEpiphanySceneAction::JobInterrupt,
ThreadEpiphanySceneAction::Propose,