Type Epiphany launch protocol params

This commit is contained in:
Meta Vens 2026-05-12 23:28:53 +01:00
commit 596d5a7fb0
6 changed files with 202 additions and 32 deletions

View file

@ -396,12 +396,16 @@ inventory and ledger now exist:
`epiphany-core` owns `EpiphanyRoleStatePatchDocument`, policy checks read
typed fields directly, and the app-server maps the typed document to the
legacy `ThreadEpiphanyUpdatePatch` without JSON round-tripping.
- Fifth cut landed: `ThreadEpiphanyJobLaunchParams` no longer exposes
`input_json` / `output_schema_json`. The legacy app-server protocol launch
edge now carries a typed `ThreadEpiphanyWorkerLaunchDocument` and
`output_contract_id`, then maps that document into the native
`EpiphanyWorkerLaunchDocument` for core.
Continue at the protocol edge. Replace
`ThreadEpiphanyJobLaunchParams.input_json` / `output_schema_json` with CultNet
typed launch-intent messages, then seal MCP JSON behind a CultNet MCP adapter.
Do not resume Rider, Unity, Aquarium, Face, dogfood, planning, app, skill,
marketplace, or bridge expansion until this organ is being cut cleanly.
Continue by sealing MCP JSON behind a CultNet MCP adapter, then audit heartbeat
cognition quarantine values for typing or explicit expiration. Do not resume
Rider, Unity, Aquarium, Face, dogfood, planning, app, skill, marketplace, or
bridge expansion until this organ is being cut cleanly.
The Phase 6 freshness slice is landed. It exposes read-only
`thread/epiphany/freshness` from live retrieval summaries plus graph

View file

@ -33,11 +33,11 @@ Rule: JSON is allowed at external schema/wire boundaries. Inside Epiphany, live
- Simpler architecture: role interpretation stores `AgentSelfPatch`, and only the protocol projection serializes JSON for the existing app-server surface.
- Files: `epiphany-core/src/agent_memory.rs`, `epiphany-core/src/surfaces/role_result.rs`, `vendor/codex/codex-rs/app-server/src/codex_message_processor.rs`, `vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs`.
- `vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs`
- Current mechanism: `ThreadEpiphanyRoleFinding.self_patch: Option<serde_json::Value>`, `ThreadEpiphanyJobLaunchRequest.input_json`, and `output_schema_json`.
- Current mechanism: `ThreadEpiphanyRoleFinding.self_patch: Option<serde_json::Value>` remains for the legacy projection. Launch params now use typed worker launch documents and `output_contract_id`.
- Real need: app-server compatibility while Epiphany is still hosted inside Codex.
- What breaks if deleted: frontend/server protocol compilation.
- Essential or bad ownership: temporary compatibility reliquary, not a final contract.
- Simpler architecture: CultNet schema messages generated from typed document structs.
- Simpler architecture: CultNet schema messages generated from typed document structs; keep app-server protocol only as a temporary compatibility reliquary.
## Quarantine JSON
@ -61,5 +61,6 @@ Rule: JSON is allowed at external schema/wire boundaries. Inside Epiphany, live
2. Partly done: core worker launch `input_json` and `output_schema_json` are replaced by typed launch documents and output contract ids. Remaining cut is the protocol edge.
3. Done: replace role `statePatch` JSON with typed map/planning/graph patch documents. `EpiphanyRoleFindingInterpretation` now carries `EpiphanyRoleStatePatchDocument`; app-server maps it to the legacy protocol patch without JSON round-tripping.
4. Done: replace runtime job result JSON projections. Runtime-spine results now project through typed role/reorient interpreters instead of `runtime_job_result_to_role_json` and `runtime_job_result_to_reorient_json`.
5. Seal MCP JSON behind a CultNet MCP adapter.
6. Audit heartbeat cognition values and either type them or explicitly expire them.
5. Done: replace app-server launch protocol `input_json` / `output_schema_json` with typed launch documents and `output_contract_id`.
6. Seal MCP JSON behind a CultNet MCP adapter.
7. Audit heartbeat cognition values and either type them or explicitly expire them.

Binary file not shown.

View file

@ -278,5 +278,5 @@ rejected_paths:
reason: making Epiphany cleaner inside Codex still preserves the wrong organ; Codex must be starved down to OpenAI subscription auth/model transport, while Epiphany data and subsystem communication move to typed CultCache/CultNet.
current_status:
summary: The Codex starvation pass has four receipts. `notes/codex-auth-spine-inventory.md` maps the minimal vendored OpenAI auth/model-call spine; `notes/json-contamination-ledger.md` classifies the major Epiphany JSON uses by allowed edge, quarantine, or internal contamination. Role-result `selfPatch` and `statePatch` now use typed Epiphany documents internally. Core worker launch now uses `EpiphanyWorkerLaunchDocument` plus `output_contract_id` instead of `input_json` / `output_schema_json`. Runtime job results now project through typed role/reorient interpreters instead of JSON round-tripping. Legacy app-server JSON remains only at hostile ingress/protocol quarantine pending CultNet replacement.
next_action: Continue the liberation pass at the protocol edge: replace `ThreadEpiphanyJobLaunchParams.input_json` / `output_schema_json` with CultNet typed launch-intent messages, then seal MCP JSON behind a CultNet MCP adapter instead of letting Codex app/tool plumbing own it.
summary: The Codex starvation pass has five receipts. `notes/codex-auth-spine-inventory.md` maps the minimal vendored OpenAI auth/model-call spine; `notes/json-contamination-ledger.md` classifies the major Epiphany JSON uses by allowed edge, quarantine, or internal contamination. Role-result `selfPatch` and `statePatch` now use typed Epiphany documents internally. Core worker launch and app-server launch params now use typed worker launch documents plus `output_contract_id` instead of `input_json` / `output_schema_json`. Runtime job results now project through typed role/reorient interpreters instead of JSON round-tripping. Legacy app-server JSON remains in smaller compatibility pockets pending CultNet replacement.
next_action: Continue the liberation pass by sealing MCP JSON behind a CultNet MCP adapter instead of letting Codex app/tool plumbing own it, then audit heartbeat cognition quarantine values for typing or explicit expiration.

View file

@ -5492,16 +5492,118 @@ pub struct ThreadEpiphanyJobLaunchParams {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub linked_graph_node_ids: Vec<String>,
pub instruction: String,
#[ts(type = "unknown")]
pub input_json: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable, type = "unknown | null")]
pub output_schema_json: Option<serde_json::Value>,
pub launch_document: ThreadEpiphanyWorkerLaunchDocument,
pub output_contract_id: String,
#[serde(default)]
#[ts(optional = nullable)]
pub max_runtime_seconds: Option<u64>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase", tag = "documentKind")]
#[ts(export_to = "v2/")]
pub enum ThreadEpiphanyWorkerLaunchDocument {
Role(ThreadEpiphanyRoleWorkerLaunchDocument),
Reorient(ThreadEpiphanyReorientWorkerLaunchDocument),
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadEpiphanyRoleWorkerLaunchDocument {
pub thread_id: String,
pub role_id: String,
pub state_revision: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub objective: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub active_subgoal_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub active_subgoals: Vec<CoreEpiphanySubgoal>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub active_graph_node_ids: Vec<String>,
#[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 scratch: Option<CoreEpiphanyScratchPad>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub invariants: Vec<CoreEpiphanyInvariant>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub graphs: Option<CoreEpiphanyGraphs>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub recent_evidence: Vec<CoreEpiphanyEvidenceRecord>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub recent_observations: Vec<CoreEpiphanyObservation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub graph_frontier: Option<CoreEpiphanyGraphFrontier>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub graph_checkpoint: Option<CoreEpiphanyGraphCheckpoint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub planning: Option<CoreEpiphanyPlanningState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub churn: Option<CoreEpiphanyChurnState>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadEpiphanyReorientWorkerLaunchDocument {
pub thread_id: String,
pub mode: String,
pub checkpoint_id: String,
pub checkpoint_kind: String,
pub checkpoint_disposition: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub checkpoint_focus: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub checkpoint_summary: Option<String>,
pub checkpoint_next_action: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub checkpoint_open_questions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub checkpoint_evidence_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub checkpoint_code_refs: Vec<CoreEpiphanyCodeRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub decision_reasons: Vec<String>,
pub decision_note: String,
pub pressure_level: String,
pub retrieval_status: String,
pub graph_status: String,
pub watcher_status: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub checkpoint_dirty_paths: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub checkpoint_changed_paths: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub scratch: Option<CoreEpiphanyScratchPad>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub graphs: Option<CoreEpiphanyGraphs>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub recent_evidence: Vec<CoreEpiphanyEvidenceRecord>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub recent_observations: Vec<CoreEpiphanyObservation>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub active_frontier_node_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>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]

View file

@ -207,6 +207,7 @@ use codex_app_server_protocol::ThreadEpiphanyReorientResultResponse;
use codex_app_server_protocol::ThreadEpiphanyReorientResultStatus;
use codex_app_server_protocol::ThreadEpiphanyReorientSource;
use codex_app_server_protocol::ThreadEpiphanyReorientStateStatus;
use codex_app_server_protocol::ThreadEpiphanyReorientWorkerLaunchDocument;
use codex_app_server_protocol::ThreadEpiphanyRetrievalFreshness;
use codex_app_server_protocol::ThreadEpiphanyRetrievalFreshnessStatus;
use codex_app_server_protocol::ThreadEpiphanyRetrieveIndexSummary;
@ -228,6 +229,7 @@ use codex_app_server_protocol::ThreadEpiphanyRoleResultStatus;
use codex_app_server_protocol::ThreadEpiphanyRoleSelfPersistenceReview;
use codex_app_server_protocol::ThreadEpiphanyRoleSelfPersistenceStatus;
use codex_app_server_protocol::ThreadEpiphanyRoleStatus;
use codex_app_server_protocol::ThreadEpiphanyRoleWorkerLaunchDocument;
use codex_app_server_protocol::ThreadEpiphanyRolesSource;
use codex_app_server_protocol::ThreadEpiphanyScene;
use codex_app_server_protocol::ThreadEpiphanySceneAction;
@ -255,6 +257,7 @@ use codex_app_server_protocol::ThreadEpiphanyViewPlanning;
use codex_app_server_protocol::ThreadEpiphanyViewReorient;
use codex_app_server_protocol::ThreadEpiphanyViewResponse;
use codex_app_server_protocol::ThreadEpiphanyViewRoles;
use codex_app_server_protocol::ThreadEpiphanyWorkerLaunchDocument;
use codex_app_server_protocol::ThreadForkParams;
use codex_app_server_protocol::ThreadForkResponse;
use codex_app_server_protocol::ThreadIncrementElicitationParams;
@ -6385,8 +6388,8 @@ impl CodexMessageProcessor {
linked_subgoal_ids,
linked_graph_node_ids,
instruction,
input_json,
output_schema_json: _,
launch_document,
output_contract_id,
max_runtime_seconds,
} = params;
@ -6411,19 +6414,7 @@ impl CodexMessageProcessor {
}
};
let launch_document =
match serde_json::from_value::<EpiphanyWorkerLaunchDocument>(input_json) {
Ok(document) => document,
Err(err) => {
self.send_invalid_request_error(
request_id,
format!("invalid Epiphany job launch document: {err}"),
)
.await;
return;
}
};
let output_contract_id = launch_document.output_contract_id().to_string();
let launch_document = map_core_worker_launch_document(launch_document);
let changed_fields = epiphany_job_launch_changed_fields();
let launched = match thread
.epiphany_launch_job(EpiphanyJobLaunchRequest {
@ -13412,6 +13403,78 @@ fn map_protocol_update_patch(patch: EpiphanyRoleStatePatchDocument) -> ThreadEpi
}
}
fn map_core_worker_launch_document(
document: ThreadEpiphanyWorkerLaunchDocument,
) -> EpiphanyWorkerLaunchDocument {
match document {
ThreadEpiphanyWorkerLaunchDocument::Role(document) => {
EpiphanyWorkerLaunchDocument::Role(map_core_role_worker_launch_document(document))
}
ThreadEpiphanyWorkerLaunchDocument::Reorient(document) => {
EpiphanyWorkerLaunchDocument::Reorient(map_core_reorient_worker_launch_document(
document,
))
}
}
}
fn map_core_role_worker_launch_document(
document: ThreadEpiphanyRoleWorkerLaunchDocument,
) -> EpiphanyRoleWorkerLaunchDocument {
EpiphanyRoleWorkerLaunchDocument {
thread_id: document.thread_id,
role_id: document.role_id,
state_revision: document.state_revision,
objective: document.objective,
active_subgoal_id: document.active_subgoal_id,
active_subgoals: document.active_subgoals,
active_graph_node_ids: document.active_graph_node_ids,
investigation_checkpoint: document.investigation_checkpoint,
scratch: document.scratch,
invariants: document.invariants,
graphs: document.graphs,
recent_evidence: document.recent_evidence,
recent_observations: document.recent_observations,
graph_frontier: document.graph_frontier,
graph_checkpoint: document.graph_checkpoint,
planning: document.planning,
churn: document.churn,
}
}
fn map_core_reorient_worker_launch_document(
document: ThreadEpiphanyReorientWorkerLaunchDocument,
) -> EpiphanyReorientWorkerLaunchDocument {
EpiphanyReorientWorkerLaunchDocument {
thread_id: document.thread_id,
mode: document.mode,
checkpoint_id: document.checkpoint_id,
checkpoint_kind: document.checkpoint_kind,
checkpoint_disposition: document.checkpoint_disposition,
checkpoint_focus: document.checkpoint_focus,
checkpoint_summary: document.checkpoint_summary,
checkpoint_next_action: document.checkpoint_next_action,
checkpoint_open_questions: document.checkpoint_open_questions,
checkpoint_evidence_ids: document.checkpoint_evidence_ids,
checkpoint_code_refs: document.checkpoint_code_refs,
decision_reasons: document.decision_reasons,
decision_note: document.decision_note,
pressure_level: document.pressure_level,
retrieval_status: document.retrieval_status,
graph_status: document.graph_status,
watcher_status: document.watcher_status,
checkpoint_dirty_paths: document.checkpoint_dirty_paths,
checkpoint_changed_paths: document.checkpoint_changed_paths,
scratch: document.scratch,
graphs: document.graphs,
recent_evidence: document.recent_evidence,
recent_observations: document.recent_observations,
active_frontier_node_ids: document.active_frontier_node_ids,
linked_subgoal_ids: document.linked_subgoal_ids,
linked_graph_node_ids: document.linked_graph_node_ids,
}
}
fn core_state_patch_from_protocol(
patch: &ThreadEpiphanyUpdatePatch,
) -> EpiphanyRoleStatePatchDocument {