Seal Epiphany agent jobs projection

This commit is contained in:
Meta Vens 2026-05-06 11:59:31 +01:00
commit a134e32fa2
5 changed files with 27 additions and 384 deletions

View file

@ -24,7 +24,7 @@ The spine exists in more than twenty-five live paths:
- 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, investigation-checkpoint summary counts, observation/evidence summaries, churn, live/stored source, and available control-plane actions without mutating or persisting state in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:340), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:3924), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4115), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10757).
- 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 specialist work. Durable `job_bindings` now act as a thin Epiphany-owned launcher seam with launcher id, authority scope, backend kind, and backend job id. New bindings use the `heartbeat` backend and app-server opens matching typed runtime-spine job receipts under `state/runtime-spine.msgpack`; old `agent_jobs` bindings remain a legacy read overlay only.
- bound-job progress notifications: app-server still declares experimental `thread/epiphany/jobsUpdated` for legacy Codex evacuation telemetry. It can translate old `agent_job_progress:{json}` background events for old bindings, but heartbeat-owned activation is now the live Epiphany path.
- bound-job progress notifications: app-server still declares experimental `thread/epiphany/jobsUpdated` as sealed legacy Codex evacuation telemetry, but the handler no longer emits Epiphany updates from old `agent_job_progress:{json}` background events. Heartbeat-owned activation and typed runtime-spine receipts are now the live Epiphany path.
- explicit job control: app-server declares experimental `thread/epiphany/jobLaunch` and `thread/epiphany/jobInterrupt`, routes them through core launch/interrupt handlers that persist heartbeat-backed durable `job_bindings` revisions through the authoritative Epiphany state path, and emit `thread/epiphany/stateUpdated` with source `jobLaunch` or `jobInterrupt` instead of creating SQLite `agent_jobs` rows or pretending jobs reflection is the scheduler.
- freshness reflection: app-server declares experimental read-only `thread/epiphany/freshness`, reads the same live/stored thread view used by scene/jobs/context, fills live retrieval state for loaded threads, and derives one bounded retrieval/graph freshness lens plus, for loaded threads, watcher-backed invalidation inputs with watched root, changed paths, mapped graph-node hits, active-frontier hits, and revision/source identity without mutating, scheduling, notifying, or performing automatic semantic invalidation in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:350), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4167), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4259), [epiphany_invalidation.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/epiphany_invalidation.rs:19), [epiphany_invalidation.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/epiphany_invalidation.rs:67), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4219), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11073).
- targeted context reflection: app-server declares experimental read-only `thread/epiphany/context`, reads the same live/stored Epiphany state view used by scene/jobs, and derives a bounded state shard containing selected graph nodes/edges, links, active frontier, checkpoint, the full durable investigation-checkpoint packet, observations, direct evidence, linked evidence, and missing requested ids without retrieval, proposal, promotion, notification, mutation, or persistence in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:355), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4253), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4261), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11152).
@ -1108,7 +1108,7 @@ The app-server handler:
### Output
The response is a typed progress board derived from the current Epiphany machine. The Phase 6 jobs smoke proved that missing-state jobs can still reflect retrieval while graph/remap and verification stay blocked, ready-state jobs expose revision identity and launcher metadata, a bound specialist runtime job surfaces `running` plus launcher id, authority scope, backend kind/job id, runtime job id, and active worker thread id, no `thread/epiphany/stateUpdated` or `thread/epiphany/jobsUpdated` notification is emitted by the read, and final thread state revision does not change.
The response is a typed progress board derived from the current Epiphany machine. The jobs projection reflects retrieval while graph/remap and verification stay blocked when state is missing, ready-state jobs expose revision identity and launcher metadata, heartbeat-backed specialist bindings project as pending heartbeat turns, stale `agent_jobs` bindings are sealed instead of crawled through SQLite, no `thread/epiphany/stateUpdated` or `thread/epiphany/jobsUpdated` notification is emitted by the read, and final thread state revision does not change.
### Invariant
@ -1465,7 +1465,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 bounded graph traversal read, not the first planning projection, not the first context-pressure gauge, not the first durable investigation checkpoint packet, not the first bounded reorientation verdict, not the first thin launcher seam, not the first explicit launch/interrupt authority surface over that seam, not the first runtime consumer that can act on the reorientation verdict without becoming a hidden tyrant, not the first read-back surface for that worker's findings, not the first explicit acceptance write for completed findings, not the first read-only CRRC coordinator recommendation, not the first role ownership board, not the first fixed role specialist launch/read-back templates, not the first review-gated modeling statePatch acceptance path, not the first planning synthesis lane, and no longer the first dogfood-facing operator view over the landed loop. Those exist. Proposal now has the first bits of map memory, focus, and selection hygiene: it can auto-select a bounded evidence-backed observation cluster when ids are omitted, requires accepting recent evidence behind selected observations, prioritizes stronger selected observations for proposal wording, reuses existing architecture nodes by concrete code-ref/path/id checks before creating new path nodes, can rescue unanchored graph nodes through strict unique semantic overlap, follows graph links and incident edges into the frontier, and reports match-kind-aware churn pressure from the actual proposal shape. Distillation now also has the first source-output-aware teeth for tool/model summaries. Promotion now notices risky deltas instead of just checking the shape of the envelope, treats graph expansion as risky even if the patch tries to mumble "low pressure" while dragging a new wall into the blueprint, token-matches strong verifier kinds so `contest` cannot sneak past as `test`, and validates investigation checkpoints against known evidence before the packet can be inked. Successful update/promote writes now emit `thread/epiphany/stateUpdated` with `source: "update"` or `source: "promote"`, event-level revision, and typed changed fields so clients no longer need to poll blindly, infer the cause from timing, or diff the whole state to know which top-level sections moved. Runtime `agent_job_progress` background events now also emit `thread/epiphany/jobsUpdated` with changed bound-job payloads so clients no longer need to poll `thread/epiphany/jobs` just to watch one real owner move, the bound jobs now reflect launcher id, authority scope, and backend kind/job id instead of treating raw runtime pointers as the architecture, `jobLaunch` / `jobInterrupt` can create or tear down those bindings without pretending reflection is authority, `reorientLaunch` can launch one fixed `reorient-worker` job from the checkpoint verdict without turning the verdict itself into a writer, `reorientResult` can read the worker's structured output back as a reviewable finding without promoting it, `reorientAccept` can explicitly bank a completed finding into typed observation/evidence plus optional scratch/checkpoint state, `roleLaunch` can launch fixed Imagination/planning, modeling/checkpoint, and verification/review specialists over that same bounded seam, `roleResult` can read their structured outputs back without mutation, `roleAccept` can apply reviewed Imagination planning patches and reviewed modeling graph/checkpoint patches through the normal state validator, and `crrc` can recommend the next explicit action over that loop without doing the action itself. The scene, jobs, roles, role-result, role-accept, freshness, context, graphQuery, planning, pressure, reorient, crrc, reorient-result, and MVP status surfaces give clients compact or bounded views without making the GUI/client authoritative. The remaining missing organs are automatic graph invalidation, scheduler automation if dogfood proves it is needed, and richer product ergonomics after the MVP loop has been tested by a human. GitHub Issues import is a future adapter, but it is deliberately deferred until the backlog source is fresh enough to be worth importing.
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 freshness lens, not the first watcher-backed invalidation read, not the first targeted graph/evidence context read, not the first bounded graph traversal read, not the first planning projection, not the first context-pressure gauge, not the first durable investigation checkpoint packet, not the first bounded reorientation verdict, not the first thin launcher seam, not the first explicit launch/interrupt authority surface over that seam, not the first runtime consumer that can act on the reorientation verdict without becoming a hidden tyrant, not the first read-back surface for that worker's findings, not the first explicit acceptance write for completed findings, not the first read-only CRRC coordinator recommendation, not the first role ownership board, not the first fixed role specialist launch/read-back templates, not the first review-gated modeling statePatch acceptance path, not the first planning synthesis lane, and no longer the first dogfood-facing operator view over the landed loop. Those exist. Proposal now has the first bits of map memory, focus, and selection hygiene: it can auto-select a bounded evidence-backed observation cluster when ids are omitted, requires accepting recent evidence behind selected observations, prioritizes stronger selected observations for proposal wording, reuses existing architecture nodes by concrete code-ref/path/id checks before creating new path nodes, can rescue unanchored graph nodes through strict unique semantic overlap, follows graph links and incident edges into the frontier, and reports match-kind-aware churn pressure from the actual proposal shape. Distillation now also has the first source-output-aware teeth for tool/model summaries. Promotion now notices risky deltas instead of just checking the shape of the envelope, treats graph expansion as risky even if the patch tries to mumble "low pressure" while dragging a new wall into the blueprint, token-matches strong verifier kinds so `contest` cannot sneak past as `test`, and validates investigation checkpoints against known evidence before the packet can be inked. Successful update/promote writes now emit `thread/epiphany/stateUpdated` with `source: "update"` or `source: "promote"`, event-level revision, and typed changed fields so clients no longer need to poll blindly, infer the cause from timing, or diff the whole state to know which top-level sections moved. Bound jobs reflect launcher id, authority scope, and backend kind/job id instead of treating raw runtime pointers as the architecture; stale `agent_jobs` bindings are sealed rather than live-crawled. `jobLaunch` / `jobInterrupt` can create or tear down those bindings without pretending reflection is authority, `reorientLaunch` can launch one fixed `reorient-worker` job from the checkpoint verdict without turning the verdict itself into a writer, `reorientResult` can read the worker's structured output back as a reviewable finding without promoting it, `reorientAccept` can explicitly bank a completed finding into typed observation/evidence plus optional scratch/checkpoint state, `roleLaunch` can launch fixed Imagination/planning, modeling/checkpoint, and verification/review specialists over that same bounded seam, `roleResult` can read their structured outputs back without mutation, `roleAccept` can apply reviewed Imagination planning patches and reviewed modeling graph/checkpoint patches through the normal state validator, and `crrc` can recommend the next explicit action over that loop without doing the action itself. The scene, jobs, roles, role-result, role-accept, freshness, context, graphQuery, planning, pressure, reorient, crrc, reorient-result, and MVP status surfaces give clients compact or bounded views without making the GUI/client authoritative. The remaining missing organs are automatic graph invalidation, scheduler automation if dogfood proves it is needed, and richer product ergonomics after the MVP loop has been tested by a human. GitHub Issues import is a future adapter, but it is deliberately deferred until the backlog source is fresh enough to be worth importing.
```text
model/tool observations
@ -1479,7 +1479,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, traverse bounded graph neighborhoods and path/symbol matches from the map, render typed graph state in the GUI through the adjacent EpiphanyGraph viewer, project planning captures/backlog/roadmap/objective drafts without adopting them, synthesize planning material through a review-gated Imagination lane, project context pressure from token telemetry, bank an in-flight investigation packet in durable typed state, reflect that packet back into prompt/scene/context, turn those signals into a read-only resume-versus-regather verdict, recommend the next explicit CRRC action over the verdict and worker/result state, print that recommendation in a dogfood operator view, explicitly launch one bounded `reorient-worker` job from that verdict, read its result back for review, explicitly accept a completed finding into typed state, retrieve evidence for the map, draft one explicit observation/evidence patch, derive a bounded graph/frontier/churn candidate from verified evidence-backed observations with code refs, choose a small coherent observation set when the caller does not provide one, favor the strongest selected observation when drafting that candidate, reject or accept a verified candidate, apply explicit map/evidence/churn/planning edits, notify clients after successful writes, inspect Rider/source-context status through an operator-safe bridge, capture Rider file/selection/symbol packets as artifacts, and sanity-check those edits before they hit the ledger. It still does not invalidate stale graph regions automatically, run scheduler automation over the fixed lanes, adopt objective drafts without a human gate, coordinate broad specialist ownership, or build-verify the Rider plugin scaffold on this machine. Teeth are installed; chewing is supervised; the first bell rings; the first wall board exists; the first shift board exists; the first freshness clipboard exists; the first door sensor now exists too; the first binder lens exists; the first graph-walk lens exists; the first planning wall exists; the first pressure gauge exists; the first ember packet exists; the first verdict slip now exists too; the first bounded launch lever and stop switch now exist too; the first explicit wakeup-hand now exists too; the first coordinator clipboard now exists too; the first operator readout exists too; Imagination now has a fenced workbench; Rider now has a first source-context hand; the GUI graph is no longer dead paper. The next slice is build-verifying or packaging the Rider plugin scaffold once Gradle/wrapper support exists, installing Unity `6000.1.10f1`, and running the next bridge-equipped Aetheria dogfood pass.
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, project a read-only freshness lens over retrieval and graph staleness, layer watcher-backed invalidation telemetry onto that lens for loaded threads, project a targeted graph/evidence context shard from the map, traverse bounded graph neighborhoods and path/symbol matches from the map, render typed graph state in the GUI through the adjacent EpiphanyGraph viewer, project planning captures/backlog/roadmap/objective drafts without adopting them, synthesize planning material through a review-gated Imagination lane, project context pressure from token telemetry, bank an in-flight investigation packet in durable typed state, reflect that packet back into prompt/scene/context, turn those signals into a read-only resume-versus-regather verdict, recommend the next explicit CRRC action over the verdict and worker/result state, print that recommendation in a dogfood operator view, explicitly launch one bounded `reorient-worker` job from that verdict, read its result back for review, explicitly accept a completed finding into typed state, retrieve evidence for the map, draft one explicit observation/evidence patch, derive a bounded graph/frontier/churn candidate from verified evidence-backed observations with code refs, choose a small coherent observation set when the caller does not provide one, favor the strongest selected observation when drafting that candidate, reject or accept a verified candidate, apply explicit map/evidence/churn/planning edits, notify clients after successful writes, inspect Rider/source-context status through an operator-safe bridge, capture Rider file/selection/symbol packets as artifacts, and sanity-check those edits before they hit the ledger. It still does not invalidate stale graph regions automatically, run scheduler automation over the fixed lanes, adopt objective drafts without a human gate, coordinate broad specialist ownership, or build-verify the Rider plugin scaffold on this machine. Teeth are installed; chewing is supervised; the first bell rings; the first wall board exists; the first shift board exists; the first freshness clipboard exists; the first door sensor now exists too; the first binder lens exists; the first graph-walk lens exists; the first planning wall exists; the first pressure gauge exists; the first ember packet exists; the first verdict slip now exists too; the first bounded launch lever and stop switch now exist too; the first explicit wakeup-hand now exists too; the first coordinator clipboard now exists too; the first operator readout exists too; Imagination now has a fenced workbench; Rider now has a first source-context hand; the GUI graph is no longer dead paper. The next slice is build-verifying or packaging the Rider plugin scaffold once Gradle/wrapper support exists, installing Unity `6000.1.10f1`, and running the next bridge-equipped Aetheria dogfood pass.
The role board, fixed specialist templates, first role accept paths, and bounded role self-memory path now exist too. Current Epiphany can project implementation, Imagination/planning, modeling/checkpoint, verification/review, and reorientation ownership through `thread/epiphany/roles`, explicitly launch fixed Imagination/modeling/verification workers, read their findings back, apply reviewed Imagination planning patches and reviewed modeling graph/checkpoint patches through `thread/epiphany/roleAccept`, review optional `selfPatch` lane-memory requests with reasons, apply accepted requests to Ghostlight-shaped role dossiers, and keep semantic acceptance review-gated. What remains is dogfooding the operator loop until real product friction, not imagined architecture guilt, tells us the next organ.

View file

@ -118,7 +118,7 @@ The current spine, blessed but not yet finished:
- read-only CRRC coordinator recommendation through `thread/epiphany/crrc`
- CRRC now recognizes already accepted reorientation findings so `reorientAccept` does not leave the operator stuck on a repeat `acceptReorientResult` recommendation.
- thin launcher metadata in durable `jobBindings`
- live bound-job progress notification through `thread/epiphany/jobsUpdated`
- `thread/epiphany/jobsUpdated` remains protocol shape only for sealed legacy evacuation telemetry; app-server no longer emits Epiphany updates from old runtime `agent_job_progress` events
- read-only compact reflection through `thread/epiphany/scene`
- read-only job/progress reflection through `thread/epiphany/jobs`, with durable launcher metadata plus live runtime `agent_jobs` overlay
- read-only role ownership reflection through `thread/epiphany/roles`

Binary file not shown.

View file

@ -80,7 +80,7 @@ accepted_design:
- Epiphany is a fork-level modeling architecture, not merely a Codex collaboration mode.
- Authoritative Epiphany state lives in Codex protocol/core session and rollout state.
- The prompt reads typed state through a bounded `<epiphany_state>` developer fragment.
- Clients read typed state through `Thread.epiphanyState`, compact reflections such as `thread/epiphany/scene`, `thread/epiphany/jobs`, and `thread/epiphany/freshness`, targeted state shards through `thread/epiphany/context`, bounded graph traversal through `thread/epiphany/graphQuery`, durable planning projection through `thread/epiphany/planning`, runtime pressure through `thread/epiphany/pressure`, bounded CRRC reorientation verdicts through `thread/epiphany/reorient`, read-only CRRC next-action recommendations through `thread/epiphany/crrc`, role ownership through `thread/epiphany/roles`, reviewable role output through `thread/epiphany/roleResult`, and live bound-job progress through `thread/epiphany/jobsUpdated`.
- Clients read typed state through `Thread.epiphanyState`, compact reflections such as `thread/epiphany/scene`, `thread/epiphany/jobs`, and `thread/epiphany/freshness`, targeted state shards through `thread/epiphany/context`, bounded graph traversal through `thread/epiphany/graphQuery`, durable planning projection through `thread/epiphany/planning`, runtime pressure through `thread/epiphany/pressure`, bounded CRRC reorientation verdicts through `thread/epiphany/reorient`, read-only CRRC next-action recommendations through `thread/epiphany/crrc`, role ownership through `thread/epiphany/roles`, and reviewable role output through `thread/epiphany/roleResult`.
- Durable typed state writes route through `thread/epiphany/update`, accepted `thread/epiphany/promote`, `thread/epiphany/reorientAccept`, `thread/epiphany/roleAccept`, or the bounded `thread/epiphany/jobLaunch`, `thread/epiphany/jobInterrupt`, `thread/epiphany/reorientLaunch`, and `thread/epiphany/roleLaunch` authority surfaces when they mutate state or `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/graphQuery`, `thread/epiphany/planning`, `thread/epiphany/pressure`, and `thread/epiphany/reorient` stay read-only.
- `thread/epiphany/index` writes the retrieval catalog, not durable Epiphany understanding; catalog is auspex output, not doctrine.
@ -96,7 +96,7 @@ accepted_design:
- EpiphanyAgent no longer carries the embedded `apps/epiphany-gui` source tree. Backend artifact directories and action tools still use the historical `.epiphany-gui` contract until that backend naming seam is deliberately renamed.
- Durable `job_bindings` now form a thin Epiphany-owned launcher seam: new specialist/reorient launches write heartbeat-backed bindings with launcher identity, authority scope, backend kind, and backend job id, and open matching typed runtime-spine job receipts under `state/runtime-spine.msgpack`. Interrupt clears typed binding authority without inspecting or cancelling old Codex `agent_jobs` rows. The older runtime-agent-job pointer remains schema compatibility only; new Epiphany launches do not create Codex `agent_jobs` rows.
- `thread/epiphany/jobs` is a read-only job/progress reflection: it derives indexing, remap, verification, and specialist slots from authoritative state and retrieval summaries, projects heartbeat-backed bindings as pending heartbeat turns, and only treats `agent_jobs` as a legacy read overlay for old bindings. It is not the scheduler, notification stream, or durable runtime job table.
- `thread/epiphany/jobsUpdated` is legacy Codex evacuation telemetry only: it can still translate old runtime `agent_job_progress:{json}` background events for old bindings, but heartbeat-owned Epiphany activation is the live path.
- `thread/epiphany/jobsUpdated` is sealed legacy Codex evacuation telemetry. The handler no longer emits Epiphany updates from old runtime `agent_job_progress:{json}` background events; heartbeat/runtime-spine is the live activation and result path.
- `thread/epiphany/freshness` is the first read-only freshness surface: it derives retrieval and graph staleness from live retrieval summaries plus frontier/churn state, and for loaded threads reflects watcher-backed invalidation inputs with changed paths plus mapped graph/frontier hits, without mutating, scheduling, or performing automatic semantic invalidation.
- `thread/epiphany/context` is the first targeted read-only state-shard surface: it selects graph nodes/edges, active frontier, checkpoint, observations, and evidence from authoritative typed state without mutating or notifying.
- `thread/epiphany/graphQuery` is the first read-only graph traversal surface over authoritative typed state: it returns explicit nodes, path/symbol matches, bounded neighbors, and frontier neighborhoods with architecture/dataflow links without mutating, notifying, indexing, scheduling, proposing, or promoting.
@ -171,7 +171,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, graph traversal, durable planning projection, current-context pressure, reorientation policy, and CRRC coordinator recommendation surfaces, plus durable investigation checkpointing for compaction-safe planning, live bound-job progress notifications, a thin launcher seam in durable job bindings, explicit launch/interrupt authority over that seam, and a bounded reorient-guided worker launch surface.
- Phase 6: read-only scene, job/progress, freshness with watcher-backed invalidation inputs, targeted context, graph traversal, durable planning projection, current-context pressure, reorientation policy, and CRRC coordinator recommendation surfaces, plus durable investigation checkpointing for compaction-safe planning, a thin launcher seam in durable job bindings, explicit launch/interrupt authority over that seam, and a bounded reorient-guided worker launch surface.
- Phase 6 read-back: read-only reorient-worker result reflection through `thread/epiphany/reorientResult`.
- Phase 6 acceptance: explicit reorient-worker finding acceptance through `thread/epiphany/reorientAccept`.
- Phase 6 dogfood view: first CLI operator status over scene/pressure/reorient/jobs/result/CRRC.

View file

@ -193,7 +193,6 @@ use codex_app_server_protocol::ThreadEpiphanyJobsParams;
use codex_app_server_protocol::ThreadEpiphanyJobsResponse;
use codex_app_server_protocol::ThreadEpiphanyJobsSource;
use codex_app_server_protocol::ThreadEpiphanyJobsUpdatedNotification;
use codex_app_server_protocol::ThreadEpiphanyJobsUpdatedSource;
use codex_app_server_protocol::ThreadEpiphanyPlanningParams;
use codex_app_server_protocol::ThreadEpiphanyPlanningResponse;
use codex_app_server_protocol::ThreadEpiphanyPlanningSummary;
@ -490,7 +489,6 @@ use codex_rollout::state_db::StateDbHandle;
use codex_rollout::state_db::get_state_db;
use codex_rollout::state_db::reconcile_rollout;
use codex_state::AgentJobItemStatus;
use codex_state::AgentJobProgress;
use codex_state::AgentJobStatus;
use codex_state::StateRuntime;
use codex_state::ThreadMetadata;
@ -17622,19 +17620,7 @@ fn extend_unique_strings(target: &mut Vec<String>, values: impl IntoIterator<Ite
}
#[derive(Debug, Default, Clone)]
struct EpiphanyJobLauncherResolution {
agent_jobs_available: bool,
snapshots_by_binding_id: HashMap<String, EpiphanyJobBackendSnapshot>,
}
#[derive(Debug, Clone)]
struct EpiphanyJobBackendSnapshot {
status: AgentJobStatus,
progress: AgentJobProgress,
updated_at_unix_seconds: i64,
last_error: Option<String>,
active_thread_ids: Vec<String>,
}
struct EpiphanyJobLauncherResolution;
fn binding_launcher_job_id(binding: &EpiphanyJobBinding) -> Option<&str> {
binding.launcher_job_id.as_deref()
@ -17659,44 +17645,11 @@ fn binding_agent_jobs_job_id(binding: &EpiphanyJobBinding) -> Option<&str> {
}
pub(crate) async fn thread_epiphany_jobs_updated_notification_for_agent_job_progress(
thread_id: ThreadId,
thread: &CodexThread,
agent_job_id: &str,
_thread_id: ThreadId,
_thread: &CodexThread,
_agent_job_id: &str,
) -> Option<ThreadEpiphanyJobsUpdatedNotification> {
let state = thread.epiphany_state().await?;
let binding_ids = state
.job_bindings
.iter()
.filter(|binding| binding_agent_jobs_job_id(binding) == Some(agent_job_id))
.map(|binding| binding.id.clone())
.collect::<HashSet<_>>();
if binding_ids.is_empty() {
return None;
}
let state_db_ctx = thread.epiphany_state_runtime().await?;
let retrieval_override = if state.retrieval.is_none() && binding_ids.contains("retrieval-index")
{
Some(thread.epiphany_retrieval_state().await)
} else {
None
};
let job_resolution =
load_epiphany_job_launcher_snapshots(Some(&state_db_ctx), Some(&state)).await;
let jobs = map_epiphany_jobs(Some(&state), retrieval_override.as_ref(), &job_resolution)
.into_iter()
.filter(|job| binding_ids.contains(job.id.as_str()))
.collect::<Vec<_>>();
if jobs.is_empty() {
return None;
}
Some(ThreadEpiphanyJobsUpdatedNotification {
thread_id: thread_id.to_string(),
source: ThreadEpiphanyJobsUpdatedSource::RuntimeProgress,
state_revision: Some(state.revision),
jobs,
})
None
}
pub(crate) async fn maybe_run_epiphany_coordinator_automation_for_turn_boundary(
@ -17950,67 +17903,16 @@ pub(crate) async fn maybe_run_epiphany_pre_compaction_checkpoint_intervention_fo
}
async fn load_epiphany_job_launcher_snapshots(
state_db_ctx: Option<&StateDbHandle>,
state: Option<&EpiphanyThreadState>,
_state_db_ctx: Option<&StateDbHandle>,
_state: Option<&EpiphanyThreadState>,
) -> EpiphanyJobLauncherResolution {
let Some(state) = state else {
return EpiphanyJobLauncherResolution::default();
};
let Some(state_db_ctx) = state_db_ctx else {
return EpiphanyJobLauncherResolution {
agent_jobs_available: false,
snapshots_by_binding_id: HashMap::new(),
};
};
let mut resolution = EpiphanyJobLauncherResolution {
agent_jobs_available: true,
snapshots_by_binding_id: HashMap::new(),
};
for binding in &state.job_bindings {
let Some(agent_job_id) = binding_agent_jobs_job_id(binding) else {
continue;
};
let Ok(Some(job)) = state_db_ctx.get_agent_job(agent_job_id).await else {
continue;
};
let Ok(progress) = state_db_ctx.get_agent_job_progress(agent_job_id).await else {
continue;
};
let active_thread_ids = state_db_ctx
.list_agent_job_items(
agent_job_id,
Some(AgentJobItemStatus::Running),
/*limit*/ Some(64),
)
.await
.map(|items| {
unique_strings(items.into_iter().filter_map(|item| item.assigned_thread_id))
})
.unwrap_or_default();
resolution.snapshots_by_binding_id.insert(
binding.id.clone(),
EpiphanyJobBackendSnapshot {
status: job.status,
progress,
updated_at_unix_seconds: job.updated_at.timestamp(),
last_error: job.last_error,
active_thread_ids,
},
);
}
resolution
EpiphanyJobLauncherResolution::default()
}
fn map_epiphany_jobs(
state: Option<&EpiphanyThreadState>,
retrieval_override: Option<&EpiphanyRetrievalState>,
job_resolution: &EpiphanyJobLauncherResolution,
_job_resolution: &EpiphanyJobLauncherResolution,
) -> Vec<ThreadEpiphanyJob> {
let mut jobs = vec![
map_epiphany_index_job(state, retrieval_override),
@ -18024,13 +17926,10 @@ fn map_epiphany_jobs(
};
for binding in &state.job_bindings {
let snapshot = job_resolution
.snapshots_by_binding_id
.get(binding.id.as_str());
let replacement = if let Some(existing) = jobs.iter().find(|job| job.id == binding.id) {
overlay_epiphany_job_binding(existing.clone(), binding, snapshot, job_resolution)
overlay_epiphany_job_binding(existing.clone(), binding)
} else {
map_epiphany_bound_job(binding, snapshot, job_resolution)
map_epiphany_bound_job(binding)
};
if let Some(existing) = jobs.iter_mut().find(|job| job.id == binding.id) {
@ -18043,11 +17942,7 @@ fn map_epiphany_jobs(
jobs
}
fn map_epiphany_bound_job(
binding: &EpiphanyJobBinding,
snapshot: Option<&EpiphanyJobBackendSnapshot>,
job_resolution: &EpiphanyJobLauncherResolution,
) -> ThreadEpiphanyJob {
fn map_epiphany_bound_job(binding: &EpiphanyJobBinding) -> ThreadEpiphanyJob {
overlay_epiphany_job_binding(
ThreadEpiphanyJob {
id: binding.id.clone(),
@ -18074,16 +17969,12 @@ fn map_epiphany_bound_job(
linked_graph_node_ids: binding.linked_graph_node_ids.clone(),
},
binding,
snapshot,
job_resolution,
)
}
fn overlay_epiphany_job_binding(
mut job: ThreadEpiphanyJob,
binding: &EpiphanyJobBinding,
snapshot: Option<&EpiphanyJobBackendSnapshot>,
job_resolution: &EpiphanyJobLauncherResolution,
) -> ThreadEpiphanyJob {
job.kind = map_core_epiphany_job_kind(binding.kind);
job.scope = binding.scope.clone();
@ -18110,27 +18001,6 @@ fn overlay_epiphany_job_binding(
return job;
}
if let Some(snapshot) = snapshot {
let effective_status = effective_agent_job_status(snapshot.status, &snapshot.progress);
job.status = map_thread_epiphany_job_status(effective_status);
job.items_processed = Some(progress_processed_items(&snapshot.progress));
job.items_total = Some(progress_total_items(&snapshot.progress));
job.progress_note = Some(render_agent_job_progress_note(snapshot, effective_status));
job.last_checkpoint_at_unix_seconds = Some(snapshot.updated_at_unix_seconds);
job.active_thread_ids = snapshot.active_thread_ids.clone();
job.blocking_reason = match effective_status {
AgentJobStatus::Failed => snapshot.last_error.clone().or(job.blocking_reason),
AgentJobStatus::Cancelled => Some(
snapshot
.last_error
.clone()
.unwrap_or_else(|| "Bound runtime agent job was cancelled.".to_string()),
),
_ => None,
};
return job;
}
if binding_backend_kind(binding) == Some(CoreEpiphanyJobBackendKind::Heartbeat)
&& binding_backend_job_id(binding).is_some()
{
@ -18147,43 +18017,15 @@ fn overlay_epiphany_job_binding(
if let Some(agent_job_id) = binding_agent_jobs_job_id(binding) {
job.status = ThreadEpiphanyJobStatus::Blocked;
job.blocking_reason = Some(if job_resolution.agent_jobs_available {
format!(
"Bound agent_jobs backend job {:?} was not found in state runtime.",
agent_job_id
)
} else {
"State runtime is unavailable, so agent_jobs backend bindings cannot be resolved."
.to_string()
});
job.blocking_reason = Some(format!(
"Legacy agent_jobs binding {:?} is sealed; Epiphany activation now belongs to heartbeat/runtime-spine.",
agent_job_id
));
}
job
}
fn effective_agent_job_status(
status: AgentJobStatus,
progress: &AgentJobProgress,
) -> AgentJobStatus {
if matches!(status, AgentJobStatus::Pending | AgentJobStatus::Running)
&& progress.pending_items == 0
&& progress.running_items == 0
&& progress.total_items > 0
&& progress
.completed_items
.saturating_add(progress.failed_items)
>= progress.total_items
{
if progress.failed_items > 0 {
AgentJobStatus::Failed
} else {
AgentJobStatus::Completed
}
} else {
status
}
}
fn map_core_epiphany_job_backend_kind(
kind: CoreEpiphanyJobBackendKind,
) -> ThreadEpiphanyJobBackendKind {
@ -18202,64 +18044,6 @@ fn map_core_epiphany_job_kind(kind: CoreEpiphanyJobKind) -> ThreadEpiphanyJobKin
}
}
fn map_thread_epiphany_job_status(status: AgentJobStatus) -> ThreadEpiphanyJobStatus {
match status {
AgentJobStatus::Pending => ThreadEpiphanyJobStatus::Pending,
AgentJobStatus::Running => ThreadEpiphanyJobStatus::Running,
AgentJobStatus::Completed => ThreadEpiphanyJobStatus::Completed,
AgentJobStatus::Failed => ThreadEpiphanyJobStatus::Failed,
AgentJobStatus::Cancelled => ThreadEpiphanyJobStatus::Cancelled,
}
}
fn progress_total_items(progress: &AgentJobProgress) -> u32 {
u32::try_from(progress.total_items).unwrap_or(u32::MAX)
}
fn progress_processed_items(progress: &AgentJobProgress) -> u32 {
u32::try_from(
progress
.completed_items
.saturating_add(progress.failed_items),
)
.unwrap_or(u32::MAX)
}
fn render_agent_job_progress_note(
snapshot: &EpiphanyJobBackendSnapshot,
effective_status: AgentJobStatus,
) -> String {
let progress = &snapshot.progress;
match effective_status {
AgentJobStatus::Pending => format!(
"Runtime agent job is pending with {} pending, {} running, {} completed, and {} failed item(s).",
progress.pending_items,
progress.running_items,
progress.completed_items,
progress.failed_items
),
AgentJobStatus::Running => format!(
"Runtime agent job is running with {} pending, {} running, {} completed, and {} failed item(s).",
progress.pending_items,
progress.running_items,
progress.completed_items,
progress.failed_items
),
AgentJobStatus::Completed => format!(
"Runtime agent job completed all {} item(s).",
progress.total_items
),
AgentJobStatus::Failed => format!(
"Runtime agent job failed with {} completed and {} failed item(s).",
progress.completed_items, progress.failed_items
),
AgentJobStatus::Cancelled => format!(
"Runtime agent job was cancelled with {} pending, {} completed, and {} failed item(s).",
progress.pending_items, progress.completed_items, progress.failed_items
),
}
}
fn map_epiphany_index_job(
state: Option<&EpiphanyThreadState>,
retrieval_override: Option<&EpiphanyRetrievalState>,
@ -23289,86 +23073,6 @@ mod tests {
assert_eq!(jobs[2].status, ThreadEpiphanyJobStatus::Blocked);
}
#[test]
fn map_epiphany_jobs_overlays_bound_runtime_agent_job_progress() {
let state = codex_protocol::protocol::EpiphanyThreadState {
job_bindings: vec![codex_protocol::protocol::EpiphanyJobBinding {
id: "specialist-work".to_string(),
kind: codex_protocol::protocol::EpiphanyJobKind::Specialist,
scope: "role-scoped specialist work".to_string(),
owner_role: "epiphany-harness".to_string(),
launcher_job_id: Some("launcher-specialist-1".to_string()),
authority_scope: Some("epiphany.specialist".to_string()),
backend_kind: Some(codex_protocol::protocol::EpiphanyJobBackendKind::AgentJobs),
backend_job_id: Some("job-specialist-1".to_string()),
runtime_agent_job_id: Some("job-specialist-1".to_string()),
linked_subgoal_ids: vec!["phase-6".to_string()],
linked_graph_node_ids: vec!["job-surface".to_string()],
progress_note: Some("Bound to a real runtime job.".to_string()),
blocking_reason: None,
}],
..Default::default()
};
let resolution = EpiphanyJobLauncherResolution {
agent_jobs_available: true,
snapshots_by_binding_id: HashMap::from([(
"specialist-work".to_string(),
EpiphanyJobBackendSnapshot {
status: AgentJobStatus::Running,
progress: AgentJobProgress {
total_items: 3,
pending_items: 1,
running_items: 1,
completed_items: 1,
failed_items: 0,
},
updated_at_unix_seconds: 1_744_600_000,
last_error: None,
active_thread_ids: vec!["worker-thread-1".to_string()],
},
)]),
};
let jobs = map_epiphany_jobs(Some(&state), None, &resolution);
let specialist = jobs
.iter()
.find(|job| job.id == "specialist-work")
.expect("specialist slot should exist");
assert_eq!(specialist.status, ThreadEpiphanyJobStatus::Running);
assert_eq!(
specialist.launcher_job_id.as_deref(),
Some("launcher-specialist-1")
);
assert_eq!(
specialist.authority_scope.as_deref(),
Some("epiphany.specialist")
);
assert_eq!(
specialist.backend_kind,
Some(ThreadEpiphanyJobBackendKind::AgentJobs)
);
assert_eq!(
specialist.backend_job_id.as_deref(),
Some("job-specialist-1")
);
assert_eq!(
specialist.runtime_agent_job_id.as_deref(),
Some("job-specialist-1")
);
assert_eq!(specialist.items_processed, Some(1));
assert_eq!(specialist.items_total, Some(3));
assert_eq!(
specialist.active_thread_ids,
vec!["worker-thread-1".to_string()]
);
assert_eq!(specialist.linked_subgoal_ids, vec!["phase-6".to_string()]);
assert_eq!(
specialist.linked_graph_node_ids,
vec!["job-surface".to_string()]
);
}
#[test]
fn map_epiphany_jobs_projects_heartbeat_binding_as_pending_without_agent_jobs() {
let state = codex_protocol::protocol::EpiphanyThreadState {
@ -23396,10 +23100,7 @@ mod tests {
let jobs = map_epiphany_jobs(
Some(&state),
None,
&EpiphanyJobLauncherResolution {
agent_jobs_available: false,
snapshots_by_binding_id: HashMap::new(),
},
&EpiphanyJobLauncherResolution::default(),
);
let specialist = jobs
.iter()
@ -23504,61 +23205,6 @@ mod tests {
assert!(note.contains("Modeling role specialist completed"));
}
#[test]
fn map_epiphany_jobs_treats_stale_active_backend_job_as_completed() {
let state = codex_protocol::protocol::EpiphanyThreadState {
job_bindings: vec![codex_protocol::protocol::EpiphanyJobBinding {
id: "specialist-work".to_string(),
kind: codex_protocol::protocol::EpiphanyJobKind::Specialist,
scope: "role-scoped specialist work".to_string(),
owner_role: "epiphany-harness".to_string(),
launcher_job_id: Some("launcher-specialist-1".to_string()),
authority_scope: Some("epiphany.specialist".to_string()),
backend_kind: Some(codex_protocol::protocol::EpiphanyJobBackendKind::AgentJobs),
backend_job_id: Some("job-specialist-1".to_string()),
runtime_agent_job_id: Some("job-specialist-1".to_string()),
linked_subgoal_ids: Vec::new(),
linked_graph_node_ids: Vec::new(),
progress_note: None,
blocking_reason: None,
}],
..Default::default()
};
let resolution = EpiphanyJobLauncherResolution {
agent_jobs_available: true,
snapshots_by_binding_id: HashMap::from([(
"specialist-work".to_string(),
EpiphanyJobBackendSnapshot {
status: AgentJobStatus::Running,
progress: AgentJobProgress {
total_items: 1,
pending_items: 0,
running_items: 0,
completed_items: 1,
failed_items: 0,
},
updated_at_unix_seconds: 1_744_600_000,
last_error: None,
active_thread_ids: Vec::new(),
},
)]),
};
let jobs = map_epiphany_jobs(Some(&state), None, &resolution);
let specialist = jobs
.iter()
.find(|job| job.id == "specialist-work")
.expect("specialist slot should exist");
assert_eq!(specialist.status, ThreadEpiphanyJobStatus::Completed);
assert_eq!(specialist.items_processed, Some(1));
assert_eq!(specialist.items_total, Some(1));
assert_eq!(
specialist.progress_note.as_deref(),
Some("Runtime agent job completed all 1 item(s).")
);
}
#[test]
fn map_epiphany_jobs_blocks_missing_bound_runtime_agent_job() {
let state = codex_protocol::protocol::EpiphanyThreadState {
@ -23579,10 +23225,7 @@ mod tests {
}],
..Default::default()
};
let resolution = EpiphanyJobLauncherResolution {
agent_jobs_available: true,
snapshots_by_binding_id: HashMap::new(),
};
let resolution = EpiphanyJobLauncherResolution::default();
let jobs = map_epiphany_jobs(Some(&state), None, &resolution);
let specialist = jobs
@ -23595,7 +23238,7 @@ mod tests {
specialist
.blocking_reason
.as_deref()
.is_some_and(|reason| reason.contains("not found in state runtime"))
.is_some_and(|reason| reason.contains("activation now belongs to heartbeat"))
);
}