diff --git a/apps/decodex/src/agent/app_server.rs b/apps/decodex/src/agent/app_server.rs index 757016e..a9d5e49 100644 --- a/apps/decodex/src/agent/app_server.rs +++ b/apps/decodex/src/agent/app_server.rs @@ -308,6 +308,7 @@ pub(crate) struct AppServerRunRequest<'a> { pub(crate) continuation_user_input: Option, pub(crate) activity_marker_path: Option, pub(crate) resume_thread_id: Option, + pub(crate) ephemeral_thread: bool, pub(crate) command_exec_health_check: Option, pub(crate) dynamic_tool_handler: Option<&'a dyn DynamicToolHandler>, pub(crate) continuation_guard: Option<&'a dyn TurnContinuationGuard>, @@ -924,6 +925,7 @@ pub(crate) fn probe_app_server(listen: &str) -> crate::prelude::Result() -> super::AppServerRunRequest<'a> { continuation_user_input: None, activity_marker_path: None, resume_thread_id: None, + ephemeral_thread: false, command_exec_health_check: None, dynamic_tool_handler: None, continuation_guard: None, @@ -366,6 +368,18 @@ fn minimal_run_request<'a>() -> super::AppServerRunRequest<'a> { } } +#[test] +fn synthetic_probe_thread_start_is_ephemeral_when_requested() { + let mut request = minimal_run_request(); + + request.ephemeral_thread = true; + + let start = super::build_thread_start_request(&request).expect("request should build"); + let value = serde_json::to_value(&start).expect("thread start request should serialize"); + + assert_eq!(value["ephemeral"], true); +} + #[test] fn command_exec_health_check_uses_bounded_standalone_request() { let health_check = CommandExecHealthCheck { @@ -1258,6 +1272,7 @@ fn live_app_server_resume_round_trip_updates_marker_and_state() { )), activity_marker_path: Some(marker_path.clone()), resume_thread_id: None, + ephemeral_thread: false, command_exec_health_check: None, dynamic_tool_handler: Some(&handler), continuation_guard: Some(&guard), @@ -1301,6 +1316,7 @@ fn live_app_server_resume_round_trip_updates_marker_and_state() { continuation_user_input: None, activity_marker_path: Some(marker_path.clone()), resume_thread_id: Some(first_result.thread_id.clone()), + ephemeral_thread: false, command_exec_health_check: None, dynamic_tool_handler: Some(&handler), continuation_guard: None, diff --git a/apps/decodex/src/orchestrator/execution.rs b/apps/decodex/src/orchestrator/execution.rs index 8c66062..a318944 100644 --- a/apps/decodex/src/orchestrator/execution.rs +++ b/apps/decodex/src/orchestrator/execution.rs @@ -452,6 +452,7 @@ where )), activity_marker_path: Some(issue_run.worktree.path.clone()), resume_thread_id: resolve_resume_thread_id(state_store, issue_run)?, + ephemeral_thread: false, command_exec_health_check: None, dynamic_tool_handler: Some(&decodex_tool_bridge), continuation_guard: Some(&continuation_guard), diff --git a/docs/spec/app-server.md b/docs/spec/app-server.md index bd87269..2b1bbfd 100644 --- a/docs/spec/app-server.md +++ b/docs/spec/app-server.md @@ -167,6 +167,7 @@ The MVP thread start request owns these fields: - `cwd` - `dynamicTools` when the run exposes issue-scoped tracker tools - `developerInstructions` +- `ephemeral` only for synthetic Decodex probe threads Decodex must not inject project-owned config, model, personality, service-tier, sandbox, or approval-policy overrides into `thread/start`. Child runs inherit runtime defaults from the active Codex runtime. @@ -302,6 +303,8 @@ The `decodex probe` command must verify at least: 5. The local client can complete the bounded app-server capability preflight after `initialize` and before `thread/start`. 6. The local client can complete one bounded standalone `command/exec` health check after `initialize`. -7. The local client can complete one `dynamicTools -> item/tool/call -> response` round trip and still finish with the expected final output. +7. The local client can complete one ephemeral + `dynamicTools -> item/tool/call -> response` round trip and still finish with the + expected final output without materializing a probe thread on disk. The probe command is the first gate before deeper orchestrator logic depends on the protocol.