Add Epiphany runtime job bindings

This commit is contained in:
Meta Vens 2026-04-26 12:31:28 +01:00
commit 2c067eca30
35 changed files with 1822 additions and 56 deletions

View file

@ -47,7 +47,7 @@ The rule is:
| `thread/epiphany/promote` | verifier gate | landed | Rejects or applies candidates through the durable update path. |
| `thread/epiphany/stateUpdated` | notification | landed | Emits updated projected state, source, revision, and changed fields after successful update/promote writes. |
| `thread/epiphany/scene` | read-only reflection | landed, live-smoked | Compact client scene derived from authoritative Epiphany state, including checkpoint summary reflection. |
| `thread/epiphany/jobs` | read-only reflection | landed, live-smoked | Derived indexing, remap, verification, and specialist-progress slots from typed state and retrieval summaries. |
| `thread/epiphany/jobs` | read-only reflection | landed, live-smoked | Derived indexing, remap, verification, and specialist-progress slots from typed state and retrieval summaries, with durable `jobBindings` plus live runtime `agent_jobs` overlay when a real owner exists. |
| `thread/epiphany/freshness` | read-only reflection | landed, live-smoked | Retrieval and graph freshness lens derived from retrieval summaries plus graph frontier/churn state, with watcher-backed invalidation inputs for loaded threads. |
| `thread/epiphany/context` | read-only reflection | landed, live-smoked | Targeted state shard for graph nodes/edges, active frontier, graph checkpoint, investigation checkpoint, observations, and evidence. |
| `thread/epiphany/pressure` | read-only reflection | landed, live-smoked | Context-pressure gauge derived from token telemetry and recorded auto-compact/context limits. |
@ -107,6 +107,7 @@ The important categories are:
- graph frontier and checkpoint
- retrieval summary
- investigation checkpoint
- job bindings
- scratch
- observations
- recent evidence
@ -129,8 +130,8 @@ Metaphor is compression after source context. It is not decoration for guesses.
These are not landed yet:
- live typed job/progress state for running indexing, remap, verification, and specialist work
- `thread/epiphany/jobsUpdated` or equivalent progress notifications
- an Epiphany-owned long-running job launcher beyond binding to existing runtime `agent_jobs`
- richer evidence-range and graph-shard inspection beyond the landed context shard
- automatic watcher-driven graph/retrieval/invariant invalidation policy on top of the landed freshness reflection
- automatic tool-output observation promotion
@ -146,10 +147,11 @@ and a verification story.
The first read-only job/progress reflection is landed as `thread/epiphany/jobs`.
It reports derived slots for retrieval indexing, graph remap, invariant
verification, and future specialist work. It does not start, schedule, persist,
or notify jobs.
verification, and specialist work, and can overlay durable `jobBindings` onto
live runtime `agent_jobs` snapshots. It does not start, schedule, create, or
notify jobs.
The next missing organ is live observable long-running work.
The next missing organ is live progress notification for bound runtime work.
Future live job state should describe work like:
@ -166,8 +168,10 @@ Minimum useful fields:
- `scope`
- `ownerRole`
- `status`
- `runtimeAgentJobId`
- `itemsProcessed`
- `itemsTotal`
- `activeThreadIds`
- `progressNote`
- `lastCheckpointAt`
- `blockingReason`

View file

@ -23,7 +23,7 @@ The spine exists in seventeen live paths:
- explicit state updates: app-server routes `thread/epiphany/update` through a loaded `CodexThread` update method that rejects malformed appended observation/evidence graph records and structurally invalid replacement fields, including investigation checkpoints that cite missing evidence, mutates live `SessionState` only after validation, bumps the revision, and persists an immediate `RolloutItem::EpiphanyState` snapshot in [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4620), [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:376), [codex_thread.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/core/src/codex_thread.rs:552), and [promotion.rs](E:/Projects/EpiphanyAgent/epiphany-core/src/promotion.rs:44).
- live state notifications and write responses: app-server declares experimental `thread/epiphany/stateUpdated` and emits it with the updated typed state, typed `source`, event-level `revision`, and typed `changedFields` after successful direct updates and accepted promotions, but not after rejected promotions; direct `thread/epiphany/update` and accepted `thread/epiphany/promote` responses expose the same revision/change metadata, accepted promotions always report `Evidence` because verifier evidence is appended even when the patch evidence list is empty, and successful write response/notification states now use the same client-visible live projection as `thread/read`, including retrieval-summary backfill when durable state has no persisted retrieval metadata, in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:1113), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4354), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4419), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4428), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4555), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4608), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4652), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4693), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11327), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11370).
- scene projection: app-server declares experimental read-only `thread/epiphany/scene`, reads the same live/stored Epiphany state view used by `thread/read`, and derives a compact client scene with objective, active subgoal, invariant status counts, graph focus/counts, retrieval status, 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 future specialist work without scheduling, notifying, mutating, or persisting jobs in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:345), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4132), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4150), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11354).
- 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` can now overlay those slots onto live runtime `agent_jobs` snapshots so the surface can show real owner/progress/thread information without scheduling, notifying, mutating, or persisting runtime jobs in [common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:347), [v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4142), [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4176), and [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11998).
- 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).
- 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).
@ -996,16 +996,16 @@ A client sends experimental `thread/epiphany/jobs` with:
### Plain-language role
Job projection is the second Phase 6 reflection boundary. It gives clients a typed answer to "what Epiphany work is visible from here?" without creating a background scheduler, durable job table, or GUI-owned progress model.
Job projection is the second Phase 6 reflection boundary. It gives clients a typed answer to "what Epiphany work is visible from here?" without creating a background scheduler, GUI-owned progress model, or a second durable runtime job table.
The surface currently reports four derived slots:
The surface currently reports four base slots:
- retrieval indexing
- graph remap
- invariant/evidence verification
- future specialist work
- specialist work
The important boundary is that these are reflections over existing state. Retrieval status comes from the same retrieval summary machinery used by scene/thread reads. Remap pressure comes from graph frontier dirty paths, open questions/gaps, and churn freshness. Verification pressure comes from invariant status counts. Specialist work is explicitly `unavailable` until a scheduler exists, which is intentionally better than making clients invent one in the upholstery.
The important boundary is that these are reflections over existing state. Retrieval status comes from the same retrieval summary machinery used by scene/thread reads. Remap pressure comes from graph frontier dirty paths, open questions/gaps, and churn freshness. Verification pressure comes from invariant status counts. Durable `jobBindings` in authoritative Epiphany state can then attach owner/scope/linkage metadata and, when they point at a real runtime `agent_jobs` record, overlay live status, progress counts, runtime job id, and active worker thread ids. If the runtime seam is missing or the referenced job is gone, the slot blocks honestly instead of improvising a secret scheduler.
Protocol shape:
@ -1027,15 +1027,17 @@ Response shape:
- optional `progressNote`
- optional `lastCheckpointAtUnixSeconds`
- optional `blockingReason`
- optional `runtimeAgentJobId`
- `activeThreadIds`
- `linkedSubgoalIds`
- `linkedGraphNodeIds`
Code refs:
- [app-server-protocol/common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:344)
- [app-server-protocol/v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4100)
- [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4114)
- [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:10734)
- [app-server-protocol/common.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/common.rs:347)
- [app-server-protocol/v2.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs:4142)
- [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:4176)
- [codex_message_processor.rs](E:/Projects/EpiphanyAgent/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs:11998)
### Mechanism
@ -1046,23 +1048,24 @@ The app-server handler:
3. calls `read_thread_view(..., include_turns: false)` to reuse the live/stored Epiphany projection path.
4. if the thread is loaded and no Epiphany retrieval summary is present, asks the loaded thread for retrieval state so the index slot can still be honest before typed Epiphany state exists.
5. returns `stateRevision` only when authoritative Epiphany state exists.
6. passes the optional state and optional retrieval override into `map_epiphany_jobs`.
6. opens a live runtime state-db seam from the loaded thread when possible and falls back to direct state-runtime lookup for read-only agent-job resolution.
7. passes the optional state, optional retrieval override, and optional runtime job snapshots into `map_epiphany_jobs`.
`map_epiphany_jobs`:
1. returns a `retrieval-index` slot from retrieval status: ready becomes idle, stale becomes needed, indexing becomes running, unavailable stays unavailable.
2. returns a `graph-remap` slot that is blocked without Epiphany state and needed when frontier dirty paths, open questions/gaps, or stale graph freshness are visible.
3. returns a `verification` slot that is blocked without Epiphany state and needed when any invariant status is not accepting.
4. returns a `specialist-work` slot as unavailable with an explicit blocking reason because specialist scheduling is not landed.
5. copies active subgoal and graph-node ids into relevant slots so clients can link progress pressure back to the visible model.
1. returns base `retrieval-index`, `graph-remap`, `verification`, and `specialist-work` slots from retrieval state, frontier/churn state, invariant counts, and the no-scheduler boundary.
2. copies active subgoal and graph-node ids into relevant slots so clients can link progress pressure back to the visible model.
3. if authoritative typed state carries `jobBindings`, matches those bindings onto the base slots by id/kind.
4. when a binding carries `runtime_agent_job_id` and the state runtime can resolve it, overlays live runtime `agent_jobs` status, progress counts, progress note, runtime job id, and active worker thread ids onto the slot.
5. when the state runtime is unavailable or the referenced runtime job is missing, blocks the bound slot with an explicit reason instead of pretending the work is running.
### 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 needed remap/verification pressure, no `thread/epiphany/stateUpdated` 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 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 needed remap/verification pressure, a bound specialist runtime job surfaces `running` plus its runtime job id and active worker thread id, no `thread/epiphany/stateUpdated` notification is emitted by the read, and final thread state revision does not change.
### Invariant
Job projection is read-only. It does not start indexing, remap graphs, verify invariants, schedule specialists, mutate `SessionState`, append rollout items, or emit progress notifications. It is a board on the wall, not a payroll department with a fake mustache.
Job projection is read-only. It does not start indexing, create runtime jobs, remap graphs, verify invariants, schedule specialists, mutate `SessionState`, append rollout items, or emit progress notifications. It is a board on the wall, not a payroll department with a fake mustache.
## Flow 15: Targeted Context Shard Control Flow
@ -1275,7 +1278,7 @@ These are deliberately not shipped yet:
- no automatic watcher-driven graph or semantic invalidation.
- no code-intelligence graph.
- no specialist-agent scheduler.
- no live long-running job execution or `thread/epiphany/jobsUpdated` progress notification.
- no `thread/epiphany/jobsUpdated` progress notification or Epiphany-owned long-running job executor.
- no GUI implementation beyond the read-only scene, jobs, freshness, context, and pressure projection surfaces.
- no durable retrieval-summary write from `thread/epiphany/retrieve`.

View file

@ -57,7 +57,7 @@ The landed machine now has:
- reusable Phase 5 app-server smoke coverage in `tools/epiphany_phase5_smoke.py`
- a first Phase 6 read-only reflection surface through `thread/epiphany/scene`
- live Phase 6 scene app-server smoke coverage in `tools/epiphany_phase6_scene_smoke.py`
- read-only Phase 6 job/progress reflection through `thread/epiphany/jobs`
- read-only Phase 6 job/progress reflection through `thread/epiphany/jobs`, with durable `jobBindings` plus live runtime `agent_jobs` overlay
- live Phase 6 jobs app-server smoke coverage in `tools/epiphany_phase6_jobs_smoke.py`
- read-only Phase 6 retrieval/graph freshness reflection plus watcher-backed invalidation inputs through `thread/epiphany/freshness`
- live Phase 6 freshness app-server smoke coverage in `tools/epiphany_phase6_freshness_smoke.py`
@ -82,7 +82,7 @@ These boundaries are more important than the individual method names:
- Durable typed state writes go through `thread/epiphany/update` or accepted `thread/epiphany/promote`.
- `thread/epiphany/index` may update the semantic retrieval catalog, but it is not a hidden Epiphany-state writer.
- `thread/epiphany/scene` is a client reflection, not a second source of truth.
- `thread/epiphany/jobs` is a derived reflection, not a scheduler or durable job store.
- `thread/epiphany/jobs` is a derived reflection over retrieval summaries, typed `jobBindings`, and optional runtime `agent_jobs` snapshots, not a scheduler or durable runtime job store.
- `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.
@ -123,7 +123,7 @@ and notify typed state. It can.
The next unknowns are:
- how to expose live long-running job progress without making the GUI authoritative
- how live job progress notifications should work now that `thread/epiphany/jobs` can bind to real runtime owners without making the GUI authoritative
- how the landed watcher-backed invalidation telemetry should be consumed without turning freshness into a secret worker
- how the landed reorientation verdict should be consumed by future automatic CRRC without becoming ceremony machinery
- how much automatic CRRC coordination belongs in runtime once pressure, freshness, watcher, checkpoint, and reorientation signals all exist
@ -135,8 +135,8 @@ Phase 6 should grow observable harness state outward from the typed spine.
Useful candidates:
1. Add real long-running job ownership/progress state so `thread/epiphany/jobs` and future notifications can reflect something real.
2. Add live job progress notifications only after there is a real long-running owner to report.
1. Add live job progress notifications now that durable `jobBindings` can point at real runtime owners.
2. Decide whether future Epiphany-owned long-running work should keep parasitizing runtime `agent_jobs` or needs a stricter fork-owned launcher seam.
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`; freshness carries watcher-backed invalidation inputs for loaded threads, 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`; jobs can now overlay durable `jobBindings` onto live runtime `agent_jobs` progress for loaded threads, freshness carries watcher-backed invalidation inputs, and reorient turns checkpoint plus freshness/pressure/watcher signals into a read-only resume-versus-regather 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,7 +63,7 @@ The current spine:
- verifier-backed promotion through `thread/epiphany/promote`
- successful-write notification through `thread/epiphany/stateUpdated`
- read-only compact reflection through `thread/epiphany/scene`
- read-only job/progress reflection through `thread/epiphany/jobs`
- read-only job/progress reflection through `thread/epiphany/jobs`, with durable `jobBindings` plus live runtime `agent_jobs` overlay
- read-only retrieval/graph freshness reflection plus watcher-backed invalidation inputs through `thread/epiphany/freshness`
- read-only targeted state-shard reflection through `thread/epiphany/context`
- read-only context-pressure reflection through `thread/epiphany/pressure`
@ -208,12 +208,14 @@ watcher, and investigation-checkpoint signals to decide whether a checkpoint
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, next-action execution, and long-running
job ownership are still future work.
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.
When the user asks to continue, the next likely organ is real long-running job
ownership/progress state so `thread/epiphany/jobs` and future notifications can
reflect something real, with automatic runtime CRRC still waiting beyond that
When the user asks to continue, the next likely organ is live
`thread/epiphany/jobsUpdated` or equivalent progress notification now that
durable `jobBindings` can point at real runtime owners through
`thread/epiphany/jobs`, with automatic runtime CRRC still waiting beyond that
boundary.
Live `thread/epiphany/scene`, `thread/epiphany/jobs`,
@ -228,7 +230,7 @@ guardrails, not the next organs.
- specialist-agent scheduling
- GUI-as-source-of-truth
- automatic runtime CRRC coordinator acting on the landed reorientation verdict
- live long-running job execution or `thread/epiphany/jobsUpdated`
- Epiphany-owned long-running job execution or `thread/epiphany/jobsUpdated`
- broad event stream beyond the landed state update notification
The machine is good enough to move outward. Do not sand the same edge until the

View file

@ -42,3 +42,4 @@
{"ts": "2026-04-26T06:01:53+00:00", "type": "design", "status": "accepted", "note": "Epiphany publication stance is built-in-the-open rather than indiscriminately FOSS: use FOSS where viable, source-available licensing where economics or capability risk demand it, and commercial terms where organizations extract real enterprise value."}
{"ts": "2026-04-26T06:51:04+00:00", "type": "verification", "status": "ok", "note": "Phase 6 watcher-backed invalidation inputs landed inside read-only thread/epiphany/freshness. Loaded threads now expose runtime watcher telemetry with watched root, changed paths, mapped graph-node/frontier hits, and no state mutation, scheduling, notification, or automatic semantic invalidation. Focused protocol/app-server tests, schema fixture regeneration/checks, app-server build, and tools/epiphany_phase6_invalidation_smoke.py plus tools/epiphany_phase6_freshness_smoke.py passed."}
{"ts": "2026-04-26T10:45:36+00:00", "type": "verification", "status": "ok", "note": "Phase 6 bounded CRRC reorientation policy landed as read-only thread/epiphany/reorient. The surface consumes checkpoint, freshness, watcher, and pressure signals to return resume-versus-regather with explicit checkpoint-path/frontier reasons, without mutation, compaction, scheduling, notification, or silent continuation. Focused protocol/schema checks, app-server mapper tests/build, and tools/epiphany_phase6_reorient_smoke.py plus tools/epiphany_phase6_scene_smoke.py passed."}
{"ts": "2026-04-26T11:30:59+00:00", "type": "verification", "status": "ok", "note": "Phase 6 jobs reflection now carries durable jobBindings in EpiphanyThreadState and overlays real runtime agent_jobs progress in read-only thread/epiphany/jobs, including runtime job ids, worker thread ids, and progress counts. The live seam required direct state-runtime lookup rather than rollout get_state_db backfill. Focused protocol/core/app-server tests, schema fixture checks, app-server build, and tools/epiphany_phase6_jobs_smoke.py passed."}

View file

@ -73,14 +73,14 @@ accepted_design:
- Context pressure must become an explicit runtime signal for automatic CRRC; inference from transcript vibes, user warnings, or shrinking safe scope is useful only as a temporary human/agent heuristic.
- The harness now has an in-flight investigation checkpoint packet in durable typed state; automatic CRRC still needs runtime logic that reads the packet and decides whether to resume or force reorientation and re-gathering after compaction.
- GUI/client work must reflect or steer typed state rather than manufacture a second source of truth.
- `thread/epiphany/jobs` is the first read-only job/progress reflection: it derives indexing, remap, verification, and specialist slots from existing state; it is not a scheduler or durable job store.
- `thread/epiphany/jobs` is a read-only job/progress reflection: it derives indexing, remap, verification, and specialist slots from authoritative state and retrieval summaries, and can overlay durable `job_bindings` onto live runtime `agent_jobs` snapshots when a real owner exists; it is not a scheduler, notification stream, or durable runtime job table.
- `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/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.
- 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.
- Live long-running job execution, progress notifications, and specialist scheduling are still future work.
- Epiphany-owned long-running job execution, live job progress notifications, and specialist scheduling are still future work.
- Handoff, plan, harness-surface, and scratch files must have distinct jobs and must not become duplicate brains.
- A machine is perfect when every remaining part visibly deserves to exist; this applies to persistent memory too.
- Evidence is not an activity feed; repeated "I just did this" entries should be cut or distilled when git already proves the work.
@ -123,5 +123,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, and bounded CRRC reorientation policy are landed. `thread/epiphany/reorient` now consumes checkpoint, freshness, watcher, and pressure signals to return a read-only `resume` versus `regather` verdict with explicit checkpoint-path/frontier reasons, but it still does not mutate state, compact, schedule, notify, or silently continue work. Focused protocol/app-server checks plus scene and reorientation 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 Phase 6 organ is real long-running job ownership/progress state so `thread/epiphany/jobs` and future notifications can reflect something real, while automatic runtime CRRC remains future work that may eventually consume the landed read-only reorientation verdict. Do not restart the completed pressure, freshness, watcher-invalidation, investigation-checkpoint, or reorientation slices 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, and bounded CRRC reorientation policy are landed. `thread/epiphany/jobs` now carries durable `job_bindings` in authoritative typed state and overlays real runtime `agent_jobs` progress for bound work, including runtime job id, worker thread ids, and progress counts, without becoming a scheduler or notification stream. `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 protocol/core/app-server checks plus the live jobs smoke 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 Phase 6 organ is live `thread/epiphany/jobsUpdated` or equivalent progress notification now that durable `job_bindings` can point at real runtime owners, while automatic runtime CRRC remains future work that may eventually consume the landed read-only reorientation verdict. Do not restart the completed pressure, freshness, watcher-invalidation, investigation-checkpoint, reorientation, or landed jobs-binding slice unless a concrete regression appears.

View file

@ -3,6 +3,8 @@ from __future__ import annotations
import argparse
import json
from pathlib import Path
import sqlite3
import time
from typing import Any
from epiphany_phase5_smoke import AppServerClient
@ -25,7 +27,115 @@ JOBS_CODE_REF = {
}
def job_surface_patch() -> dict[str, Any]:
def locate_state_db(codex_home: Path) -> Path:
candidates = sorted(codex_home.glob("state_*.sqlite"))
if candidates:
return candidates[-1]
fallback = sorted(
path
for path in codex_home.glob("*.sqlite")
if "logs" not in path.name.lower()
)
if fallback:
return fallback[-1]
raise FileNotFoundError(f"no state db found under {codex_home}")
def seed_agent_job(codex_home: Path, job_id: str, running_thread_id: str) -> None:
db_path = locate_state_db(codex_home)
now = int(time.time())
connection = sqlite3.connect(db_path)
try:
with connection:
connection.execute(
"""
INSERT INTO agent_jobs (
id, name, status, instruction, output_schema_json, input_headers_json,
input_csv_path, output_csv_path, auto_export, created_at, updated_at,
started_at, completed_at, last_error
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
job_id,
"epiphany-specialist-smoke",
"running",
"Smoke-seeded runtime job for Epiphany jobs reflection.",
None,
"[]",
str(codex_home / "input.csv"),
str(codex_home / "output.csv"),
0,
now - 120,
now,
now - 90,
None,
None,
),
)
connection.executemany(
"""
INSERT INTO agent_job_items (
job_id, item_id, row_index, source_id, row_json, status, assigned_thread_id,
attempt_count, result_json, last_error, created_at, updated_at, completed_at,
reported_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
[
(
job_id,
"item-completed",
0,
"row-0",
'{"row":0}',
"completed",
None,
1,
'{"accepted":true}',
None,
now - 120,
now - 30,
now - 30,
now - 30,
),
(
job_id,
"item-running",
1,
"row-1",
'{"row":1}',
"running",
running_thread_id,
1,
None,
None,
now - 120,
now,
None,
None,
),
(
job_id,
"item-pending",
2,
"row-2",
'{"row":2}',
"pending",
None,
0,
None,
None,
now - 120,
now - 10,
None,
None,
),
],
)
finally:
connection.close()
def job_surface_patch(runtime_agent_job_id: str) -> dict[str, Any]:
return {
"objective": "Expose read-only Epiphany job/progress reflection without creating a scheduler.",
"activeSubgoalId": "phase6-jobs-smoke",
@ -68,6 +178,18 @@ def job_surface_patch() -> dict[str, Any]:
"dirty_paths": ["app-server/src/codex_message_processor.rs"],
"open_question_ids": ["q-live-progress"],
},
"jobBindings": [
{
"id": "specialist-work",
"kind": "specialist",
"scope": "role-scoped specialist work",
"owner_role": "epiphany-harness",
"runtime_agent_job_id": runtime_agent_job_id,
"linked_subgoal_ids": ["phase6-jobs-smoke"],
"linked_graph_node_ids": ["job-surface"],
"progress_note": "Bound to a real runtime specialist job.",
}
],
"churn": {
"understanding_status": "ready",
"diff_pressure": "low",
@ -126,8 +248,17 @@ def assert_ready_jobs(response: dict[str, Any]) -> None:
require(verification["itemsTotal"] == 2, "verification should count total invariants")
specialist = job_by_id(response["jobs"], "specialist-work")
require(specialist["status"] == "unavailable", "specialist scheduler should remain explicit non-flow")
require("blockingReason" in specialist, "specialist non-flow should explain itself")
require(specialist["status"] == "running", "bound specialist job should report real runtime progress")
require(
specialist["runtimeAgentJobId"] == "job-specialist-smoke",
"specialist runtime job id should surface through jobs reflection",
)
require(specialist["itemsProcessed"] == 1, "specialist job should count completed+failed items")
require(specialist["itemsTotal"] == 3, "specialist job should count total runtime items")
require(
specialist["activeThreadIds"] == ["worker-thread-specialist"],
"specialist job should expose active worker thread ids",
)
def run_smoke(args: argparse.Namespace) -> dict[str, Any]:
@ -160,6 +291,7 @@ def run_smoke(args: argparse.Namespace) -> dict[str, Any]:
)
assert started is not None
thread_id = started["thread"]["id"]
seed_agent_job(codex_home, "job-specialist-smoke", "worker-thread-specialist")
missing_notification_start = len(client.notifications)
missing_response = client.send("thread/epiphany/jobs", {"threadId": thread_id})
@ -174,7 +306,11 @@ def run_smoke(args: argparse.Namespace) -> dict[str, Any]:
update_notification_start = len(client.notifications)
update = client.send(
"thread/epiphany/update",
{"threadId": thread_id, "expectedRevision": 0, "patch": job_surface_patch()},
{
"threadId": thread_id,
"expectedRevision": 0,
"patch": job_surface_patch("job-specialist-smoke"),
},
)
assert update is not None
require(update["revision"] == 1, "job smoke patch should advance revision to 1")
@ -213,6 +349,13 @@ def run_smoke(args: argparse.Namespace) -> dict[str, Any]:
"verificationItemsTotal": job_by_id(
ready_response["jobs"], "verification"
)["itemsTotal"],
"specialistStatus": job_by_id(ready_response["jobs"], "specialist-work")["status"],
"specialistRuntimeAgentJobId": job_by_id(
ready_response["jobs"], "specialist-work"
)["runtimeAgentJobId"],
"specialistActiveThreadIds": job_by_id(
ready_response["jobs"], "specialist-work"
)["activeThreadIds"],
"jobsNotificationCount": client.count_notifications(
"thread/epiphany/stateUpdated",
start_index=jobs_notification_start,

View file

@ -1159,6 +1159,68 @@
],
"type": "string"
},
"EpiphanyJobBinding": {
"properties": {
"blocking_reason": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/EpiphanyJobKind"
},
"linked_graph_node_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"linked_subgoal_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"owner_role": {
"type": "string"
},
"progress_note": {
"type": [
"string",
"null"
]
},
"runtime_agent_job_id": {
"type": [
"string",
"null"
]
},
"scope": {
"type": "string"
}
},
"required": [
"id",
"kind",
"owner_role",
"scope"
],
"type": "object"
},
"EpiphanyJobKind": {
"enum": [
"indexing",
"remap",
"verification",
"specialist"
],
"type": "string"
},
"EpiphanyModeState": {
"properties": {
"kind": {
@ -3849,6 +3911,15 @@
}
]
},
"jobBindings": {
"items": {
"$ref": "#/definitions/EpiphanyJobBinding"
},
"type": [
"array",
"null"
]
},
"mode": {
"anyOf": [
{

View file

@ -1419,6 +1419,68 @@
],
"type": "string"
},
"EpiphanyJobBinding": {
"properties": {
"blocking_reason": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/EpiphanyJobKind"
},
"linked_graph_node_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"linked_subgoal_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"owner_role": {
"type": "string"
},
"progress_note": {
"type": [
"string",
"null"
]
},
"runtime_agent_job_id": {
"type": [
"string",
"null"
]
},
"scope": {
"type": "string"
}
},
"required": [
"id",
"kind",
"owner_role",
"scope"
],
"type": "object"
},
"EpiphanyJobKind": {
"enum": [
"indexing",
"remap",
"verification",
"specialist"
],
"type": "string"
},
"EpiphanyModeState": {
"properties": {
"kind": {
@ -1703,6 +1765,12 @@
}
]
},
"job_bindings": {
"items": {
"$ref": "#/definitions/EpiphanyJobBinding"
},
"type": "array"
},
"last_updated_turn_id": {
"type": [
"string",
@ -3799,6 +3867,7 @@
"graphCheckpoint",
"scratch",
"investigationCheckpoint",
"jobBindings",
"observations",
"evidence",
"churn",

View file

@ -8268,6 +8268,68 @@
],
"type": "string"
},
"EpiphanyJobBinding": {
"properties": {
"blocking_reason": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/v2/EpiphanyJobKind"
},
"linked_graph_node_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"linked_subgoal_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"owner_role": {
"type": "string"
},
"progress_note": {
"type": [
"string",
"null"
]
},
"runtime_agent_job_id": {
"type": [
"string",
"null"
]
},
"scope": {
"type": "string"
}
},
"required": [
"id",
"kind",
"owner_role",
"scope"
],
"type": "object"
},
"EpiphanyJobKind": {
"enum": [
"indexing",
"remap",
"verification",
"specialist"
],
"type": "string"
},
"EpiphanyModeState": {
"properties": {
"kind": {
@ -8552,6 +8614,12 @@
}
]
},
"job_bindings": {
"items": {
"$ref": "#/definitions/v2/EpiphanyJobBinding"
},
"type": "array"
},
"last_updated_turn_id": {
"type": [
"string",
@ -15146,6 +15214,12 @@
},
"ThreadEpiphanyJob": {
"properties": {
"activeThreadIds": {
"items": {
"type": "string"
},
"type": "array"
},
"blockingReason": {
"type": [
"string",
@ -15202,6 +15276,12 @@
"null"
]
},
"runtimeAgentJobId": {
"type": [
"string",
"null"
]
},
"scope": {
"type": "string"
},
@ -15231,7 +15311,11 @@
"enum": [
"idle",
"needed",
"pending",
"running",
"completed",
"failed",
"cancelled",
"blocked",
"unavailable"
],
@ -16125,6 +16209,7 @@
"graphCheckpoint",
"scratch",
"investigationCheckpoint",
"jobBindings",
"observations",
"evidence",
"churn",
@ -16246,6 +16331,15 @@
}
]
},
"jobBindings": {
"items": {
"$ref": "#/definitions/v2/EpiphanyJobBinding"
},
"type": [
"array",
"null"
]
},
"mode": {
"anyOf": [
{

View file

@ -4828,6 +4828,68 @@
],
"type": "string"
},
"EpiphanyJobBinding": {
"properties": {
"blocking_reason": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/EpiphanyJobKind"
},
"linked_graph_node_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"linked_subgoal_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"owner_role": {
"type": "string"
},
"progress_note": {
"type": [
"string",
"null"
]
},
"runtime_agent_job_id": {
"type": [
"string",
"null"
]
},
"scope": {
"type": "string"
}
},
"required": [
"id",
"kind",
"owner_role",
"scope"
],
"type": "object"
},
"EpiphanyJobKind": {
"enum": [
"indexing",
"remap",
"verification",
"specialist"
],
"type": "string"
},
"EpiphanyModeState": {
"properties": {
"kind": {
@ -5112,6 +5174,12 @@
}
]
},
"job_bindings": {
"items": {
"$ref": "#/definitions/EpiphanyJobBinding"
},
"type": "array"
},
"last_updated_turn_id": {
"type": [
"string",
@ -13033,6 +13101,12 @@
},
"ThreadEpiphanyJob": {
"properties": {
"activeThreadIds": {
"items": {
"type": "string"
},
"type": "array"
},
"blockingReason": {
"type": [
"string",
@ -13089,6 +13163,12 @@
"null"
]
},
"runtimeAgentJobId": {
"type": [
"string",
"null"
]
},
"scope": {
"type": "string"
},
@ -13118,7 +13198,11 @@
"enum": [
"idle",
"needed",
"pending",
"running",
"completed",
"failed",
"cancelled",
"blocked",
"unavailable"
],
@ -14012,6 +14096,7 @@
"graphCheckpoint",
"scratch",
"investigationCheckpoint",
"jobBindings",
"observations",
"evidence",
"churn",
@ -14133,6 +14218,15 @@
}
]
},
"jobBindings": {
"items": {
"$ref": "#/definitions/EpiphanyJobBinding"
},
"type": [
"array",
"null"
]
},
"mode": {
"anyOf": [
{

View file

@ -418,6 +418,68 @@
],
"type": "string"
},
"EpiphanyJobBinding": {
"properties": {
"blocking_reason": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/EpiphanyJobKind"
},
"linked_graph_node_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"linked_subgoal_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"owner_role": {
"type": "string"
},
"progress_note": {
"type": [
"string",
"null"
]
},
"runtime_agent_job_id": {
"type": [
"string",
"null"
]
},
"scope": {
"type": "string"
}
},
"required": [
"id",
"kind",
"owner_role",
"scope"
],
"type": "object"
},
"EpiphanyJobKind": {
"enum": [
"indexing",
"remap",
"verification",
"specialist"
],
"type": "string"
},
"EpiphanyModeState": {
"properties": {
"kind": {
@ -702,6 +764,12 @@
}
]
},
"job_bindings": {
"items": {
"$ref": "#/definitions/EpiphanyJobBinding"
},
"type": "array"
},
"last_updated_turn_id": {
"type": [
"string",
@ -792,6 +860,7 @@
"graphCheckpoint",
"scratch",
"investigationCheckpoint",
"jobBindings",
"observations",
"evidence",
"churn",

View file

@ -866,6 +866,68 @@
],
"type": "string"
},
"EpiphanyJobBinding": {
"properties": {
"blocking_reason": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/EpiphanyJobKind"
},
"linked_graph_node_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"linked_subgoal_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"owner_role": {
"type": "string"
},
"progress_note": {
"type": [
"string",
"null"
]
},
"runtime_agent_job_id": {
"type": [
"string",
"null"
]
},
"scope": {
"type": "string"
}
},
"required": [
"id",
"kind",
"owner_role",
"scope"
],
"type": "object"
},
"EpiphanyJobKind": {
"enum": [
"indexing",
"remap",
"verification",
"specialist"
],
"type": "string"
},
"EpiphanyModeState": {
"properties": {
"kind": {
@ -1150,6 +1212,12 @@
}
]
},
"job_bindings": {
"items": {
"$ref": "#/definitions/EpiphanyJobBinding"
},
"type": "array"
},
"last_updated_turn_id": {
"type": [
"string",

View file

@ -807,6 +807,68 @@
],
"type": "string"
},
"EpiphanyJobBinding": {
"properties": {
"blocking_reason": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/EpiphanyJobKind"
},
"linked_graph_node_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"linked_subgoal_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"owner_role": {
"type": "string"
},
"progress_note": {
"type": [
"string",
"null"
]
},
"runtime_agent_job_id": {
"type": [
"string",
"null"
]
},
"scope": {
"type": "string"
}
},
"required": [
"id",
"kind",
"owner_role",
"scope"
],
"type": "object"
},
"EpiphanyJobKind": {
"enum": [
"indexing",
"remap",
"verification",
"specialist"
],
"type": "string"
},
"EpiphanyModeState": {
"properties": {
"kind": {
@ -1091,6 +1153,12 @@
}
]
},
"job_bindings": {
"items": {
"$ref": "#/definitions/EpiphanyJobBinding"
},
"type": "array"
},
"last_updated_turn_id": {
"type": [
"string",

View file

@ -807,6 +807,68 @@
],
"type": "string"
},
"EpiphanyJobBinding": {
"properties": {
"blocking_reason": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/EpiphanyJobKind"
},
"linked_graph_node_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"linked_subgoal_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"owner_role": {
"type": "string"
},
"progress_note": {
"type": [
"string",
"null"
]
},
"runtime_agent_job_id": {
"type": [
"string",
"null"
]
},
"scope": {
"type": "string"
}
},
"required": [
"id",
"kind",
"owner_role",
"scope"
],
"type": "object"
},
"EpiphanyJobKind": {
"enum": [
"indexing",
"remap",
"verification",
"specialist"
],
"type": "string"
},
"EpiphanyModeState": {
"properties": {
"kind": {
@ -1091,6 +1153,12 @@
}
]
},
"job_bindings": {
"items": {
"$ref": "#/definitions/EpiphanyJobBinding"
},
"type": "array"
},
"last_updated_turn_id": {
"type": [
"string",

View file

@ -807,6 +807,68 @@
],
"type": "string"
},
"EpiphanyJobBinding": {
"properties": {
"blocking_reason": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/EpiphanyJobKind"
},
"linked_graph_node_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"linked_subgoal_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"owner_role": {
"type": "string"
},
"progress_note": {
"type": [
"string",
"null"
]
},
"runtime_agent_job_id": {
"type": [
"string",
"null"
]
},
"scope": {
"type": "string"
}
},
"required": [
"id",
"kind",
"owner_role",
"scope"
],
"type": "object"
},
"EpiphanyJobKind": {
"enum": [
"indexing",
"remap",
"verification",
"specialist"
],
"type": "string"
},
"EpiphanyModeState": {
"properties": {
"kind": {
@ -1091,6 +1153,12 @@
}
]
},
"job_bindings": {
"items": {
"$ref": "#/definitions/EpiphanyJobBinding"
},
"type": "array"
},
"last_updated_turn_id": {
"type": [
"string",

View file

@ -866,6 +866,68 @@
],
"type": "string"
},
"EpiphanyJobBinding": {
"properties": {
"blocking_reason": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/EpiphanyJobKind"
},
"linked_graph_node_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"linked_subgoal_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"owner_role": {
"type": "string"
},
"progress_note": {
"type": [
"string",
"null"
]
},
"runtime_agent_job_id": {
"type": [
"string",
"null"
]
},
"scope": {
"type": "string"
}
},
"required": [
"id",
"kind",
"owner_role",
"scope"
],
"type": "object"
},
"EpiphanyJobKind": {
"enum": [
"indexing",
"remap",
"verification",
"specialist"
],
"type": "string"
},
"EpiphanyModeState": {
"properties": {
"kind": {
@ -1150,6 +1212,12 @@
}
]
},
"job_bindings": {
"items": {
"$ref": "#/definitions/EpiphanyJobBinding"
},
"type": "array"
},
"last_updated_turn_id": {
"type": [
"string",

View file

@ -807,6 +807,68 @@
],
"type": "string"
},
"EpiphanyJobBinding": {
"properties": {
"blocking_reason": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/EpiphanyJobKind"
},
"linked_graph_node_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"linked_subgoal_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"owner_role": {
"type": "string"
},
"progress_note": {
"type": [
"string",
"null"
]
},
"runtime_agent_job_id": {
"type": [
"string",
"null"
]
},
"scope": {
"type": "string"
}
},
"required": [
"id",
"kind",
"owner_role",
"scope"
],
"type": "object"
},
"EpiphanyJobKind": {
"enum": [
"indexing",
"remap",
"verification",
"specialist"
],
"type": "string"
},
"EpiphanyModeState": {
"properties": {
"kind": {
@ -1091,6 +1153,12 @@
}
]
},
"job_bindings": {
"items": {
"$ref": "#/definitions/EpiphanyJobBinding"
},
"type": "array"
},
"last_updated_turn_id": {
"type": [
"string",

View file

@ -866,6 +866,68 @@
],
"type": "string"
},
"EpiphanyJobBinding": {
"properties": {
"blocking_reason": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/EpiphanyJobKind"
},
"linked_graph_node_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"linked_subgoal_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"owner_role": {
"type": "string"
},
"progress_note": {
"type": [
"string",
"null"
]
},
"runtime_agent_job_id": {
"type": [
"string",
"null"
]
},
"scope": {
"type": "string"
}
},
"required": [
"id",
"kind",
"owner_role",
"scope"
],
"type": "object"
},
"EpiphanyJobKind": {
"enum": [
"indexing",
"remap",
"verification",
"specialist"
],
"type": "string"
},
"EpiphanyModeState": {
"properties": {
"kind": {
@ -1150,6 +1212,12 @@
}
]
},
"job_bindings": {
"items": {
"$ref": "#/definitions/EpiphanyJobBinding"
},
"type": "array"
},
"last_updated_turn_id": {
"type": [
"string",

View file

@ -807,6 +807,68 @@
],
"type": "string"
},
"EpiphanyJobBinding": {
"properties": {
"blocking_reason": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/EpiphanyJobKind"
},
"linked_graph_node_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"linked_subgoal_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"owner_role": {
"type": "string"
},
"progress_note": {
"type": [
"string",
"null"
]
},
"runtime_agent_job_id": {
"type": [
"string",
"null"
]
},
"scope": {
"type": "string"
}
},
"required": [
"id",
"kind",
"owner_role",
"scope"
],
"type": "object"
},
"EpiphanyJobKind": {
"enum": [
"indexing",
"remap",
"verification",
"specialist"
],
"type": "string"
},
"EpiphanyModeState": {
"properties": {
"kind": {
@ -1091,6 +1153,12 @@
}
]
},
"job_bindings": {
"items": {
"$ref": "#/definitions/EpiphanyJobBinding"
},
"type": "array"
},
"last_updated_turn_id": {
"type": [
"string",

View file

@ -807,6 +807,68 @@
],
"type": "string"
},
"EpiphanyJobBinding": {
"properties": {
"blocking_reason": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/EpiphanyJobKind"
},
"linked_graph_node_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"linked_subgoal_ids": {
"items": {
"type": "string"
},
"type": "array"
},
"owner_role": {
"type": "string"
},
"progress_note": {
"type": [
"string",
"null"
]
},
"runtime_agent_job_id": {
"type": [
"string",
"null"
]
},
"scope": {
"type": "string"
}
},
"required": [
"id",
"kind",
"owner_role",
"scope"
],
"type": "object"
},
"EpiphanyJobKind": {
"enum": [
"indexing",
"remap",
"verification",
"specialist"
],
"type": "string"
},
"EpiphanyModeState": {
"properties": {
"kind": {
@ -1091,6 +1153,12 @@
}
]
},
"job_bindings": {
"items": {
"$ref": "#/definitions/EpiphanyJobBinding"
},
"type": "array"
},
"last_updated_turn_id": {
"type": [
"string",

View file

@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { EpiphanyJobKind } from "./EpiphanyJobKind";
export type EpiphanyJobBinding = { id: string, kind: EpiphanyJobKind, scope: string, owner_role: string, runtime_agent_job_id?: string | null, linked_subgoal_ids?: Array<string>, linked_graph_node_ids?: Array<string>, progress_note?: string | null, blocking_reason?: string | null, };

View file

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

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 EpiphanyThreadState = { revision: bigint, objective?: string | null, active_subgoal_id?: string | null, subgoals?: Array<EpiphanySubgoal>, invariants?: Array<EpiphanyInvariant>, graphs?: EpiphanyGraphs, graph_frontier?: EpiphanyGraphFrontier | null, graph_checkpoint?: EpiphanyGraphCheckpoint | null, retrieval?: EpiphanyRetrievalState | null, scratch?: EpiphanyScratchPad | null, investigation_checkpoint?: EpiphanyInvestigationCheckpoint | null, observations?: Array<EpiphanyObservation>, recent_evidence?: Array<EpiphanyEvidenceRecord>, churn?: EpiphanyChurnState | null, mode?: EpiphanyModeState | null, last_updated_turn_id?: string | null, };
export type EpiphanyThreadState = { revision: bigint, objective?: string | null, active_subgoal_id?: string | null, subgoals?: Array<EpiphanySubgoal>, invariants?: Array<EpiphanyInvariant>, graphs?: EpiphanyGraphs, graph_frontier?: EpiphanyGraphFrontier | null, graph_checkpoint?: EpiphanyGraphCheckpoint | null, retrieval?: EpiphanyRetrievalState | null, scratch?: EpiphanyScratchPad | null, investigation_checkpoint?: EpiphanyInvestigationCheckpoint | null, job_bindings?: Array<EpiphanyJobBinding>, observations?: Array<EpiphanyObservation>, recent_evidence?: Array<EpiphanyEvidenceRecord>, churn?: EpiphanyChurnState | null, mode?: EpiphanyModeState | null, last_updated_turn_id?: string | null, };

View file

@ -25,6 +25,8 @@ export type { EpiphanyGraphs } from "./EpiphanyGraphs";
export type { EpiphanyInvariant } from "./EpiphanyInvariant";
export type { EpiphanyInvestigationCheckpoint } from "./EpiphanyInvestigationCheckpoint";
export type { EpiphanyInvestigationDisposition } from "./EpiphanyInvestigationDisposition";
export type { EpiphanyJobBinding } from "./EpiphanyJobBinding";
export type { EpiphanyJobKind } from "./EpiphanyJobKind";
export type { EpiphanyModeState } from "./EpiphanyModeState";
export type { EpiphanyObservation } from "./EpiphanyObservation";
export type { EpiphanyRetrievalStatus } from "./EpiphanyRetrievalStatus";

View file

@ -4,4 +4,4 @@
import type { ThreadEpiphanyJobKind } from "./ThreadEpiphanyJobKind";
import type { ThreadEpiphanyJobStatus } from "./ThreadEpiphanyJobStatus";
export type ThreadEpiphanyJob = { id: string, kind: ThreadEpiphanyJobKind, scope: string, ownerRole: string, status: ThreadEpiphanyJobStatus, itemsProcessed?: number | null, itemsTotal?: number | null, progressNote?: string | null, lastCheckpointAtUnixSeconds?: bigint | null, blockingReason?: string | null, linkedSubgoalIds?: Array<string>, linkedGraphNodeIds?: Array<string>, };
export type ThreadEpiphanyJob = { id: string, kind: ThreadEpiphanyJobKind, scope: string, ownerRole: string, status: ThreadEpiphanyJobStatus, runtimeAgentJobId?: string | null, itemsProcessed?: number | null, itemsTotal?: number | null, progressNote?: string | null, lastCheckpointAtUnixSeconds?: bigint | null, blockingReason?: string | null, activeThreadIds?: Array<string>, linkedSubgoalIds?: Array<string>, linkedGraphNodeIds?: Array<string>, };

View file

@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ThreadEpiphanyJobStatus = "idle" | "needed" | "running" | "blocked" | "unavailable";
export type ThreadEpiphanyJobStatus = "idle" | "needed" | "pending" | "running" | "completed" | "failed" | "cancelled" | "blocked" | "unavailable";

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 ThreadEpiphanyStateUpdatedField = "objective" | "activeSubgoalId" | "subgoals" | "invariants" | "graphs" | "graphFrontier" | "graphCheckpoint" | "scratch" | "investigationCheckpoint" | "observations" | "evidence" | "churn" | "mode";
export type ThreadEpiphanyStateUpdatedField = "objective" | "activeSubgoalId" | "subgoals" | "invariants" | "graphs" | "graphFrontier" | "graphCheckpoint" | "scratch" | "investigationCheckpoint" | "jobBindings" | "observations" | "evidence" | "churn" | "mode";

View file

@ -8,9 +8,10 @@ import type { EpiphanyGraphFrontier } from "../EpiphanyGraphFrontier";
import type { EpiphanyGraphs } from "../EpiphanyGraphs";
import type { EpiphanyInvariant } from "../EpiphanyInvariant";
import type { EpiphanyInvestigationCheckpoint } from "../EpiphanyInvestigationCheckpoint";
import type { EpiphanyJobBinding } from "../EpiphanyJobBinding";
import type { EpiphanyModeState } from "../EpiphanyModeState";
import type { EpiphanyObservation } from "../EpiphanyObservation";
import type { EpiphanyScratchPad } from "../EpiphanyScratchPad";
import type { EpiphanySubgoal } from "../EpiphanySubgoal";
export type ThreadEpiphanyUpdatePatch = { objective?: string | null, activeSubgoalId?: string | null, subgoals?: Array<EpiphanySubgoal> | null, invariants?: Array<EpiphanyInvariant> | null, graphs?: EpiphanyGraphs | null, graphFrontier?: EpiphanyGraphFrontier | null, graphCheckpoint?: EpiphanyGraphCheckpoint | null, scratch?: EpiphanyScratchPad | null, investigationCheckpoint?: EpiphanyInvestigationCheckpoint | null, observations?: Array<EpiphanyObservation>, evidence?: Array<EpiphanyEvidenceRecord>, churn?: EpiphanyChurnState | null, mode?: EpiphanyModeState | null, };
export type ThreadEpiphanyUpdatePatch = { objective?: string | null, activeSubgoalId?: string | null, subgoals?: Array<EpiphanySubgoal> | null, invariants?: Array<EpiphanyInvariant> | null, graphs?: EpiphanyGraphs | null, graphFrontier?: EpiphanyGraphFrontier | null, graphCheckpoint?: EpiphanyGraphCheckpoint | null, scratch?: EpiphanyScratchPad | null, investigationCheckpoint?: EpiphanyInvestigationCheckpoint | null, jobBindings?: Array<EpiphanyJobBinding> | null, observations?: Array<EpiphanyObservation>, evidence?: Array<EpiphanyEvidenceRecord>, churn?: EpiphanyChurnState | null, mode?: EpiphanyModeState | null, };

View file

@ -1820,6 +1820,7 @@ mod tests {
scope: "/workspace".to_string(),
owner_role: "epiphany-core".to_string(),
status: v2::ThreadEpiphanyJobStatus::Needed,
runtime_agent_job_id: None,
items_processed: Some(12),
items_total: None,
progress_note: Some(
@ -1827,6 +1828,7 @@ mod tests {
),
last_checkpoint_at_unix_seconds: Some(1_744_500_000),
blocking_reason: None,
active_thread_ids: Vec::new(),
linked_subgoal_ids: vec!["phase-6".to_string()],
linked_graph_node_ids: vec!["retrieval".to_string()],
}],

View file

@ -69,6 +69,7 @@ use codex_protocol::protocol::EpiphanyGraphs as CoreEpiphanyGraphs;
use codex_protocol::protocol::EpiphanyInvariant as CoreEpiphanyInvariant;
use codex_protocol::protocol::EpiphanyInvestigationCheckpoint as CoreEpiphanyInvestigationCheckpoint;
use codex_protocol::protocol::EpiphanyInvestigationDisposition as CoreEpiphanyInvestigationDisposition;
use codex_protocol::protocol::EpiphanyJobBinding as CoreEpiphanyJobBinding;
use codex_protocol::protocol::EpiphanyModeState as CoreEpiphanyModeState;
use codex_protocol::protocol::EpiphanyObservation as CoreEpiphanyObservation;
use codex_protocol::protocol::EpiphanyRetrievalStatus as CoreEpiphanyRetrievalStatus;
@ -4291,7 +4292,11 @@ pub enum ThreadEpiphanyJobKind {
pub enum ThreadEpiphanyJobStatus {
Idle,
Needed,
Pending,
Running,
Completed,
Failed,
Cancelled,
Blocked,
Unavailable,
}
@ -4307,6 +4312,9 @@ pub struct ThreadEpiphanyJob {
pub status: ThreadEpiphanyJobStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub runtime_agent_job_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub items_processed: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
@ -4321,6 +4329,8 @@ pub struct ThreadEpiphanyJob {
#[ts(optional = nullable)]
pub blocking_reason: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub active_thread_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub linked_subgoal_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub linked_graph_node_ids: Vec<String>,
@ -4727,6 +4737,9 @@ pub struct ThreadEpiphanyUpdatePatch {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub investigation_checkpoint: Option<CoreEpiphanyInvestigationCheckpoint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub job_bindings: Option<Vec<CoreEpiphanyJobBinding>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub observations: Vec<CoreEpiphanyObservation>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
@ -4780,6 +4793,7 @@ pub enum ThreadEpiphanyStateUpdatedField {
GraphCheckpoint,
Scratch,
InvestigationCheckpoint,
JobBindings,
Observations,
Evidence,
Churn,

View file

@ -402,6 +402,8 @@ use codex_protocol::protocol::ConversationStartParams;
use codex_protocol::protocol::ConversationStartTransport;
use codex_protocol::protocol::ConversationTextParams;
use codex_protocol::protocol::EpiphanyInvestigationDisposition;
use codex_protocol::protocol::EpiphanyJobBinding;
use codex_protocol::protocol::EpiphanyJobKind as CoreEpiphanyJobKind;
use codex_protocol::protocol::EpiphanyRetrievalState;
use codex_protocol::protocol::EpiphanyRetrievalStatus;
use codex_protocol::protocol::EpiphanyThreadState;
@ -429,6 +431,9 @@ use codex_rmcp_client::perform_oauth_login_return_url;
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;
use codex_state::ThreadMetadataBuilder;
@ -3398,7 +3403,7 @@ impl CodexMessageProcessor {
let loaded_thread = self.thread_manager.get_thread(thread_uuid).await.ok();
let mut state_db_ctx = loaded_thread.as_ref().and_then(|thread| thread.state_db());
if state_db_ctx.is_none() {
state_db_ctx = get_state_db(&self.config).await;
state_db_ctx = open_state_db_for_direct_thread_lookup(&self.config).await;
}
let Some(state_db_ctx) = state_db_ctx else {
self.send_internal_error(
@ -4212,6 +4217,15 @@ impl CodexMessageProcessor {
} else {
None
};
let mut state_db_ctx = loaded_thread.as_ref().and_then(|thread| thread.state_db());
if state_db_ctx.is_none() {
state_db_ctx = open_state_db_for_direct_thread_lookup(&self.config).await;
}
let job_resolution = load_epiphany_agent_job_snapshots(
state_db_ctx.as_ref(),
thread.epiphany_state.as_ref(),
)
.await;
let response = ThreadEpiphanyJobsResponse {
thread_id,
@ -4224,7 +4238,11 @@ impl CodexMessageProcessor {
.epiphany_state
.as_ref()
.map(|epiphany_state| epiphany_state.revision),
jobs: map_epiphany_jobs(thread.epiphany_state.as_ref(), retrieval_override.as_ref()),
jobs: map_epiphany_jobs(
thread.epiphany_state.as_ref(),
retrieval_override.as_ref(),
&job_resolution,
),
};
self.outgoing.send_response(request_id, response).await;
}
@ -4821,6 +4839,7 @@ impl CodexMessageProcessor {
graph_checkpoint: patch.graph_checkpoint,
scratch: patch.scratch,
investigation_checkpoint: patch.investigation_checkpoint,
job_bindings: patch.job_bindings,
observations: patch.observations,
evidence,
churn: patch.churn,
@ -4909,6 +4928,7 @@ impl CodexMessageProcessor {
graph_checkpoint: patch.graph_checkpoint,
scratch: patch.scratch,
investigation_checkpoint: patch.investigation_checkpoint,
job_bindings: patch.job_bindings,
observations: patch.observations,
evidence: patch.evidence,
churn: patch.churn,
@ -11899,16 +11919,271 @@ fn extend_unique_strings(target: &mut Vec<String>, values: impl IntoIterator<Ite
}
}
#[derive(Debug, Default, Clone)]
struct EpiphanyAgentJobResolution {
available: bool,
snapshots_by_binding_id: HashMap<String, EpiphanyAgentJobSnapshot>,
}
#[derive(Debug, Clone)]
struct EpiphanyAgentJobSnapshot {
status: AgentJobStatus,
progress: AgentJobProgress,
updated_at_unix_seconds: i64,
last_error: Option<String>,
active_thread_ids: Vec<String>,
}
async fn load_epiphany_agent_job_snapshots(
state_db_ctx: Option<&StateDbHandle>,
state: Option<&EpiphanyThreadState>,
) -> EpiphanyAgentJobResolution {
let Some(state) = state else {
return EpiphanyAgentJobResolution::default();
};
let Some(state_db_ctx) = state_db_ctx else {
return EpiphanyAgentJobResolution {
available: false,
snapshots_by_binding_id: HashMap::new(),
};
};
let mut resolution = EpiphanyAgentJobResolution {
available: true,
snapshots_by_binding_id: HashMap::new(),
};
for binding in &state.job_bindings {
let Some(runtime_agent_job_id) = binding.runtime_agent_job_id.as_deref() else {
continue;
};
let Ok(Some(job)) = state_db_ctx.get_agent_job(runtime_agent_job_id).await else {
continue;
};
let Ok(progress) = state_db_ctx
.get_agent_job_progress(runtime_agent_job_id)
.await
else {
continue;
};
let active_thread_ids = state_db_ctx
.list_agent_job_items(
runtime_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(),
EpiphanyAgentJobSnapshot {
status: job.status,
progress,
updated_at_unix_seconds: job.updated_at.timestamp(),
last_error: job.last_error,
active_thread_ids,
},
);
}
resolution
}
fn map_epiphany_jobs(
state: Option<&EpiphanyThreadState>,
retrieval_override: Option<&EpiphanyRetrievalState>,
job_resolution: &EpiphanyAgentJobResolution,
) -> Vec<ThreadEpiphanyJob> {
vec![
let mut jobs = vec![
map_epiphany_index_job(state, retrieval_override),
map_epiphany_remap_job(state),
map_epiphany_verification_job(state),
map_epiphany_specialist_job(),
]
];
let Some(state) = state else {
return 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)
} else {
map_epiphany_bound_job(binding, snapshot, job_resolution)
};
if let Some(existing) = jobs.iter_mut().find(|job| job.id == binding.id) {
*existing = replacement;
} else {
jobs.push(replacement);
}
}
jobs
}
fn map_epiphany_bound_job(
binding: &EpiphanyJobBinding,
snapshot: Option<&EpiphanyAgentJobSnapshot>,
job_resolution: &EpiphanyAgentJobResolution,
) -> ThreadEpiphanyJob {
overlay_epiphany_job_binding(
ThreadEpiphanyJob {
id: binding.id.clone(),
kind: map_core_epiphany_job_kind(binding.kind),
scope: binding.scope.clone(),
owner_role: binding.owner_role.clone(),
status: if binding.blocking_reason.is_some() {
ThreadEpiphanyJobStatus::Blocked
} else {
ThreadEpiphanyJobStatus::Idle
},
runtime_agent_job_id: binding.runtime_agent_job_id.clone(),
items_processed: None,
items_total: None,
progress_note: None,
last_checkpoint_at_unix_seconds: None,
blocking_reason: None,
active_thread_ids: Vec::new(),
linked_subgoal_ids: binding.linked_subgoal_ids.clone(),
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<&EpiphanyAgentJobSnapshot>,
job_resolution: &EpiphanyAgentJobResolution,
) -> ThreadEpiphanyJob {
job.kind = map_core_epiphany_job_kind(binding.kind);
job.scope = binding.scope.clone();
job.owner_role = binding.owner_role.clone();
job.runtime_agent_job_id = binding.runtime_agent_job_id.clone();
if !binding.linked_subgoal_ids.is_empty() {
job.linked_subgoal_ids = binding.linked_subgoal_ids.clone();
}
if !binding.linked_graph_node_ids.is_empty() {
job.linked_graph_node_ids = binding.linked_graph_node_ids.clone();
}
if let Some(progress_note) = binding.progress_note.clone() {
job.progress_note = Some(progress_note);
}
if let Some(blocking_reason) = binding.blocking_reason.clone() {
job.blocking_reason = Some(blocking_reason);
}
if let Some(snapshot) = snapshot {
job.status = map_thread_epiphany_job_status(snapshot.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));
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 snapshot.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 let Some(runtime_agent_job_id) = binding.runtime_agent_job_id.as_deref() {
job.status = ThreadEpiphanyJobStatus::Blocked;
job.blocking_reason = Some(if job_resolution.available {
format!(
"Bound runtime agent job {:?} was not found in state runtime.",
runtime_agent_job_id
)
} else {
"State runtime is unavailable, so bound runtime agent jobs cannot be resolved."
.to_string()
});
}
job
}
fn map_core_epiphany_job_kind(kind: CoreEpiphanyJobKind) -> ThreadEpiphanyJobKind {
match kind {
CoreEpiphanyJobKind::Indexing => ThreadEpiphanyJobKind::Indexing,
CoreEpiphanyJobKind::Remap => ThreadEpiphanyJobKind::Remap,
CoreEpiphanyJobKind::Verification => ThreadEpiphanyJobKind::Verification,
CoreEpiphanyJobKind::Specialist => ThreadEpiphanyJobKind::Specialist,
}
}
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: &EpiphanyAgentJobSnapshot) -> String {
let progress = &snapshot.progress;
match snapshot.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(
@ -11928,11 +12203,13 @@ fn map_epiphany_index_job(
scope: "workspace".to_string(),
owner_role: "epiphany-core".to_string(),
status: ThreadEpiphanyJobStatus::Unavailable,
runtime_agent_job_id: None,
items_processed: None,
items_total: None,
progress_note: None,
last_checkpoint_at_unix_seconds: None,
blocking_reason: Some("Retrieval state is unavailable for this thread.".to_string()),
active_thread_ids: Vec::new(),
linked_subgoal_ids,
linked_graph_node_ids,
};
@ -11959,6 +12236,7 @@ fn map_epiphany_index_job(
scope: retrieval.workspace_root.display().to_string(),
owner_role: "epiphany-core".to_string(),
status: epiphany_job_status_from_retrieval_status(retrieval.status),
runtime_agent_job_id: None,
items_processed: retrieval.indexed_file_count,
items_total: None,
progress_note: Some(progress_note),
@ -11966,6 +12244,7 @@ fn map_epiphany_index_job(
blocking_reason: (retrieval.status == EpiphanyRetrievalStatus::Unavailable).then(|| {
"Indexing requires a readable workspace and configured retrieval backend.".to_string()
}),
active_thread_ids: Vec::new(),
linked_subgoal_ids,
linked_graph_node_ids,
}
@ -12024,11 +12303,13 @@ fn map_epiphany_remap_job(state: Option<&EpiphanyThreadState>) -> ThreadEpiphany
} else {
ThreadEpiphanyJobStatus::Idle
},
runtime_agent_job_id: None,
items_processed: None,
items_total: None,
progress_note: Some(progress_note),
last_checkpoint_at_unix_seconds: None,
blocking_reason: None,
active_thread_ids: Vec::new(),
linked_subgoal_ids: epiphany_active_subgoal_ids(Some(state)),
linked_graph_node_ids: epiphany_active_graph_node_ids(Some(state)),
}
@ -12069,11 +12350,13 @@ fn map_epiphany_verification_job(state: Option<&EpiphanyThreadState>) -> ThreadE
scope: "invariants/evidence".to_string(),
owner_role: "epiphany-harness".to_string(),
status,
runtime_agent_job_id: None,
items_processed: Some(verified),
items_total: Some(total),
progress_note: Some(progress_note),
last_checkpoint_at_unix_seconds: None,
blocking_reason: None,
active_thread_ids: Vec::new(),
linked_subgoal_ids: epiphany_active_subgoal_ids(Some(state)),
linked_graph_node_ids: epiphany_active_graph_node_ids(Some(state)),
}
@ -12086,6 +12369,7 @@ fn map_epiphany_specialist_job() -> ThreadEpiphanyJob {
scope: "role-scoped specialist work".to_string(),
owner_role: "epiphany-harness".to_string(),
status: ThreadEpiphanyJobStatus::Unavailable,
runtime_agent_job_id: None,
items_processed: None,
items_total: None,
progress_note: None,
@ -12094,6 +12378,7 @@ fn map_epiphany_specialist_job() -> ThreadEpiphanyJob {
"Specialist scheduling is not landed; this slot prevents clients from inventing private scheduler state."
.to_string(),
),
active_thread_ids: Vec::new(),
linked_subgoal_ids: Vec::new(),
linked_graph_node_ids: Vec::new(),
}
@ -12111,11 +12396,13 @@ fn epiphany_blocked_state_job(
scope: scope.to_string(),
owner_role: "epiphany-harness".to_string(),
status: ThreadEpiphanyJobStatus::Blocked,
runtime_agent_job_id: None,
items_processed: None,
items_total: None,
progress_note: None,
last_checkpoint_at_unix_seconds: None,
blocking_reason: Some(blocking_reason.to_string()),
active_thread_ids: Vec::new(),
linked_subgoal_ids: Vec::new(),
linked_graph_node_ids: Vec::new(),
}
@ -12165,6 +12452,7 @@ fn thread_epiphany_patch_has_state_replacements(patch: &ThreadEpiphanyUpdatePatc
|| patch.graph_checkpoint.is_some()
|| patch.scratch.is_some()
|| patch.investigation_checkpoint.is_some()
|| patch.job_bindings.is_some()
|| patch.churn.is_some()
|| patch.mode.is_some()
}
@ -12266,6 +12554,9 @@ fn epiphany_update_patch_changed_fields(
if patch.investigation_checkpoint.is_some() {
fields.push(ThreadEpiphanyStateUpdatedField::InvestigationCheckpoint);
}
if patch.job_bindings.is_some() {
fields.push(ThreadEpiphanyStateUpdatedField::JobBindings);
}
if !patch.observations.is_empty() {
fields.push(ThreadEpiphanyStateUpdatedField::Observations);
}
@ -14490,7 +14781,7 @@ mod tests {
..Default::default()
};
let jobs = map_epiphany_jobs(Some(&state), None);
let jobs = map_epiphany_jobs(Some(&state), None, &EpiphanyAgentJobResolution::default());
assert_eq!(jobs.len(), 4);
assert_eq!(jobs[0].id, "retrieval-index");
@ -14527,7 +14818,11 @@ mod tests {
..Default::default()
};
let jobs = map_epiphany_jobs(None, Some(&retrieval));
let jobs = map_epiphany_jobs(
None,
Some(&retrieval),
&EpiphanyAgentJobResolution::default(),
);
assert_eq!(jobs[0].status, ThreadEpiphanyJobStatus::Idle);
assert_eq!(jobs[0].items_processed, Some(7));
@ -14535,6 +14830,102 @@ 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(),
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 = EpiphanyAgentJobResolution {
available: true,
snapshots_by_binding_id: HashMap::from([(
"specialist-work".to_string(),
EpiphanyAgentJobSnapshot {
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.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_blocks_missing_bound_runtime_agent_job() {
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(),
runtime_agent_job_id: Some("job-missing".to_string()),
linked_subgoal_ids: Vec::new(),
linked_graph_node_ids: Vec::new(),
progress_note: None,
blocking_reason: None,
}],
..Default::default()
};
let resolution = EpiphanyAgentJobResolution {
available: true,
snapshots_by_binding_id: HashMap::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::Blocked);
assert!(
specialist
.blocking_reason
.as_deref()
.is_some_and(|reason| reason.contains("not found in state runtime"))
);
}
#[test]
fn epiphany_update_patch_changed_fields_reports_patch_surface() {
let fields = epiphany_update_patch_changed_fields(&ThreadEpiphanyUpdatePatch {
@ -14542,6 +14933,7 @@ mod tests {
graphs: Some(Default::default()),
graph_frontier: Some(Default::default()),
investigation_checkpoint: Some(Default::default()),
job_bindings: Some(Vec::new()),
observations: vec![Default::default()],
evidence: vec![Default::default()],
churn: Some(Default::default()),
@ -14555,6 +14947,7 @@ mod tests {
ThreadEpiphanyStateUpdatedField::Graphs,
ThreadEpiphanyStateUpdatedField::GraphFrontier,
ThreadEpiphanyStateUpdatedField::InvestigationCheckpoint,
ThreadEpiphanyStateUpdatedField::JobBindings,
ThreadEpiphanyStateUpdatedField::Observations,
ThreadEpiphanyStateUpdatedField::Evidence,
ThreadEpiphanyStateUpdatedField::Churn,

View file

@ -32,6 +32,7 @@ use codex_protocol::protocol::EpiphanyGraphFrontier;
use codex_protocol::protocol::EpiphanyGraphs;
use codex_protocol::protocol::EpiphanyInvariant;
use codex_protocol::protocol::EpiphanyInvestigationCheckpoint;
use codex_protocol::protocol::EpiphanyJobBinding;
use codex_protocol::protocol::EpiphanyModeState;
use codex_protocol::protocol::EpiphanyObservation;
use codex_protocol::protocol::EpiphanyRetrievalState;
@ -112,6 +113,7 @@ pub struct EpiphanyStateUpdate {
pub graph_checkpoint: Option<EpiphanyGraphCheckpoint>,
pub scratch: Option<EpiphanyScratchPad>,
pub investigation_checkpoint: Option<EpiphanyInvestigationCheckpoint>,
pub job_bindings: Option<Vec<EpiphanyJobBinding>>,
pub observations: Vec<EpiphanyObservation>,
pub evidence: Vec<EpiphanyEvidenceRecord>,
pub churn: Option<EpiphanyChurnState>,
@ -129,6 +131,7 @@ impl EpiphanyStateUpdate {
&& self.graph_checkpoint.is_none()
&& self.scratch.is_none()
&& self.investigation_checkpoint.is_none()
&& self.job_bindings.is_none()
&& self.observations.is_empty()
&& self.evidence.is_empty()
&& self.churn.is_none()
@ -645,10 +648,44 @@ fn epiphany_state_update_validation_errors(
}
}
if let Some(job_bindings) = update.job_bindings.as_ref() {
errors.extend(validate_epiphany_job_bindings(job_bindings));
}
errors.extend(epiphany_state_replacement_validation_errors(state, update));
errors
}
fn validate_epiphany_job_bindings(job_bindings: &[EpiphanyJobBinding]) -> Vec<String> {
let mut errors = Vec::new();
let mut seen_ids = HashSet::<&str>::new();
for binding in job_bindings {
require_nonempty_update(&binding.id, "job_binding.id", &mut errors);
require_nonempty_update(&binding.scope, "job_binding.scope", &mut errors);
require_nonempty_update(&binding.owner_role, "job_binding.owner_role", &mut errors);
if let Some(runtime_agent_job_id) = binding.runtime_agent_job_id.as_deref() {
require_nonempty_update(
runtime_agent_job_id,
"job_binding.runtime_agent_job_id",
&mut errors,
);
}
if let Some(progress_note) = binding.progress_note.as_deref() {
require_nonempty_update(progress_note, "job_binding.progress_note", &mut errors);
}
if let Some(blocking_reason) = binding.blocking_reason.as_deref() {
require_nonempty_update(blocking_reason, "job_binding.blocking_reason", &mut errors);
}
if !binding.id.is_empty() && !seen_ids.insert(binding.id.as_str()) {
errors.push(format!("duplicate job binding id {:?}", binding.id));
}
}
errors
}
fn epiphany_state_replacement_validation_errors(
state: &EpiphanyThreadState,
update: &EpiphanyStateUpdate,
@ -734,6 +771,9 @@ fn apply_epiphany_state_update(
if let Some(checkpoint) = update.investigation_checkpoint {
state.investigation_checkpoint = Some(checkpoint);
}
if let Some(job_bindings) = update.job_bindings {
state.job_bindings = job_bindings;
}
if let Some(churn) = update.churn {
state.churn = Some(churn);
}
@ -760,6 +800,7 @@ mod epiphany_update_tests {
use super::*;
use codex_protocol::protocol::EpiphanyGraph;
use codex_protocol::protocol::EpiphanyGraphNode;
use codex_protocol::protocol::EpiphanyJobKind;
fn evidence(id: &str) -> EpiphanyEvidenceRecord {
EpiphanyEvidenceRecord {
@ -782,6 +823,20 @@ mod epiphany_update_tests {
}
}
fn job_binding(id: &str) -> EpiphanyJobBinding {
EpiphanyJobBinding {
id: id.to_string(),
kind: EpiphanyJobKind::Specialist,
scope: "role-scoped specialist work".to_string(),
owner_role: "epiphany-harness".to_string(),
runtime_agent_job_id: Some(format!("agent-job-{id}")),
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 agent job.".to_string()),
blocking_reason: None,
}
}
#[test]
fn apply_epiphany_state_update_replaces_typed_fields_and_prepends_evidence() {
let mut state = EpiphanyThreadState {
@ -847,6 +902,33 @@ mod epiphany_update_tests {
);
}
#[test]
fn apply_epiphany_state_update_replaces_job_bindings() {
let mut state = EpiphanyThreadState {
revision: 2,
job_bindings: vec![job_binding("old")],
..Default::default()
};
apply_epiphany_state_update(
&mut state,
EpiphanyStateUpdate {
job_bindings: Some(vec![job_binding("new")]),
..Default::default()
},
Some("turn-jobs".to_string()),
);
assert_eq!(state.revision, 3);
assert_eq!(state.job_bindings.len(), 1);
assert_eq!(state.job_bindings[0].id, "new");
assert_eq!(
state.job_bindings[0].runtime_agent_job_id.as_deref(),
Some("agent-job-new")
);
assert_eq!(state.last_updated_turn_id.as_deref(), Some("turn-jobs"));
}
#[test]
fn validate_epiphany_state_update_accepts_observation_linked_to_existing_evidence() {
let state = EpiphanyThreadState {
@ -998,6 +1080,67 @@ mod epiphany_update_tests {
.any(|error| error.contains("investigation checkpoint cites missing evidence id"))
);
}
#[test]
fn validate_epiphany_state_update_rejects_invalid_job_bindings() {
let update = EpiphanyStateUpdate {
job_bindings: Some(vec![
job_binding("dup"),
job_binding("dup"),
EpiphanyJobBinding {
id: String::new(),
kind: EpiphanyJobKind::Verification,
scope: String::new(),
owner_role: String::new(),
runtime_agent_job_id: Some(String::new()),
linked_subgoal_ids: Vec::new(),
linked_graph_node_ids: Vec::new(),
progress_note: Some(String::new()),
blocking_reason: Some(String::new()),
},
]),
..Default::default()
};
let errors =
epiphany_state_update_validation_errors(&EpiphanyThreadState::default(), &update);
assert!(
errors
.iter()
.any(|error| error.contains("duplicate job binding id"))
);
assert!(
errors
.iter()
.any(|error| error.contains("job_binding.id must not be empty"))
);
assert!(
errors
.iter()
.any(|error| error.contains("job_binding.scope must not be empty"))
);
assert!(
errors
.iter()
.any(|error| error.contains("job_binding.owner_role must not be empty"))
);
assert!(
errors
.iter()
.any(|error| error.contains("job_binding.runtime_agent_job_id must not be empty"))
);
assert!(
errors
.iter()
.any(|error| error.contains("job_binding.progress_note must not be empty"))
);
assert!(
errors
.iter()
.any(|error| error.contains("job_binding.blocking_reason must not be empty"))
);
}
}
fn pending_message_input_item(message: &ResponseItem) -> CodexResult<ResponseInputItem> {

View file

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

View file

@ -3021,6 +3021,9 @@ pub struct EpiphanyThreadState {
#[ts(type = "EpiphanyInvestigationCheckpoint | null")]
pub investigation_checkpoint: Option<EpiphanyInvestigationCheckpoint>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyJobBinding>")]
pub job_bindings: Vec<EpiphanyJobBinding>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyObservation>")]
pub observations: Vec<EpiphanyObservation>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
@ -3296,6 +3299,39 @@ pub struct EpiphanyInvestigationCheckpoint {
pub evidence_ids: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum EpiphanyJobKind {
Indexing,
Remap,
Verification,
Specialist,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
pub struct EpiphanyJobBinding {
pub id: String,
pub kind: EpiphanyJobKind,
pub scope: String,
pub owner_role: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub runtime_agent_job_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub linked_subgoal_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub linked_graph_node_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub progress_note: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub blocking_reason: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyObservation {
pub id: String,
@ -5722,6 +5758,7 @@ mod tests {
next_probe: Some("Replay after rollback and compaction".to_string()),
notes: vec!["Do not touch prompts yet".to_string()],
}),
job_bindings: Vec::new(),
observations: vec![EpiphanyObservation {
id: "obs-1".to_string(),
summary: "TurnContext already gives us the persistence seam".to_string(),