Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
984978a
docs(plan): codex sidecar lifecycle — persistent tracking, reattach, …
danshapiro Aug 10, 2026
a292c61
docs(plan): apply load-bearing validation findings to the codex sidec…
danshapiro Aug 10, 2026
064c6bd
docs(plan): fix fresh-eyes blockers — name Task 9's fixture edit, mak…
danshapiro Aug 10, 2026
6f6aa33
docs(plan): fix fresh-eyes iteration-2 blockers — de-vacuate the ws v…
danshapiro Aug 11, 2026
2f5195a
docs(plan): fix fresh-eyes iteration-3 blockers — gate the detach, cl…
danshapiro Aug 11, 2026
fc1cb90
feat(codex): durable rust-owned sidecar record store (rust-codex-side…
danshapiro Aug 11, 2026
060b34d
feat(codex): pid identity evidence + verification for sidecar records
danshapiro Aug 11, 2026
7aff6ab
feat(codex): persist terminal-pane sidecar records at spawn; detach t…
danshapiro Aug 11, 2026
e883d5a
feat(codex): record the codex session/thread id in the sidecar record
danshapiro Aug 11, 2026
1bd5fae
feat(codex): boot-time sidecar reconciler with verified claim-by-session
danshapiro Aug 11, 2026
1116255
feat(codex): reattach runtime — adopt a surviving verified sidecar in…
danshapiro Aug 11, 2026
012edeb
feat(codex): plan-aware runtime factory selects reattach over spawn
danshapiro Aug 11, 2026
0696844
test(codex): pin the true reattach-failure retry path and the selecto…
danshapiro Aug 11, 2026
2d8fe54
test(ws): e2e — codex pane restore reattaches to a surviving sidecar …
danshapiro Aug 11, 2026
273c016
feat(codex): conservative reap sweep — tracked, verified, never mid-t…
danshapiro Aug 11, 2026
384400a
feat(server): boot sidecar reconcile + reap sweep; retain adopted sid…
danshapiro Aug 11, 2026
ee36653
fix(codex): final-review hardening — handshake-correct writer probe, …
danshapiro Aug 11, 2026
2431588
fix(test): add handshake_settings to reattach e2e WsState after rebase
danshapiro Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions crates/freshell-codex/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ default = []
real-transport = ["dep:tokio-tungstenite", "dep:futures-util", "dep:libc", "tokio/net", "tokio/io-util", "tokio/process"]

[dependencies]
# Derive for the durable sidecar record rows (`sidecar_store`) — the versioned
# camelCase JSON schema a restarted server reads back (BindingRow precedent,
# pane_ledger.rs:93-130).
serde = { workspace = true }
# Dynamic JSON parsing that mirrors the TS `Record<string, unknown>` params/result model
# (JSON.parse parity, corruption-tolerant). preserve_order matches the wire object model.
serde_json = { workspace = true }
Expand All @@ -57,3 +61,8 @@ futures-util = { version = "0.3", default-features = false, features = ["sink",
# FRESHELL_CODEX_SIDECAR_ID tag (runtime.ts:494) so no orphan survives (the oracle
# `ownership.cleanup` invariant). Only needed by the real transport.
libc = { version = "0.2", optional = true }

[dev-dependencies]
# Tempdirs for the sidecar-store unit tests — no global state, nothing outside
# each test's own temp dir (never `~/.freshell/`).
tempfile = "3"
25 changes: 25 additions & 0 deletions crates/freshell-codex/src/app_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,31 @@ impl CodexAppServerClient {
.await
}

/// `thread/loaded/list` — the ids of threads this app-server currently
/// has loaded in memory (result shape `{ data: string[], nextCursor? }`,
/// contract-foundation plan §thread/loaded/list; the committed fixture
/// returns `{ data: behavior.loadedThreadIds }`). NOTE: `loaded` alone
/// does NOT mean mid-turn — idle threads stay loaded forever
/// (reports/V1.md); pair with [`Self::read_thread`]'s status. Added for
/// Task 9's sweep probe.
pub async fn list_loaded_threads(&self) -> Result<Vec<String>, CodexAppServerError> {
let result = self.request("thread/loaded/list", json!({})).await?;
// Absent/non-array `data` is NOT an empty list: the sweep probe reads
// Ok(vec![]) as proof of "reachable, no loaded threads" and may REAP
// on it — a malformed payload must fail loudly instead, sending the
// caller down the conservative writer-evidence path (final review F2).
let Some(ids) = result.get("data").and_then(Value::as_array) else {
return Err(CodexAppServerError::InvalidResponse {
method: "thread/loaded/list".to_string(),
detail: format!("expected result.data to be an array, got: {result}"),
});
};
Ok(ids
.iter()
.filter_map(|id| id.as_str().map(str::to_string))
.collect())
}

/// Send a notification frame (no response awaited) — `notify`, `client.ts:805-808`.
pub async fn notify(
&self,
Expand Down
408 changes: 386 additions & 22 deletions crates/freshell-codex/src/launch_lifecycle.rs

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions crates/freshell-codex/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ pub mod launch_lifecycle;
#[cfg(feature = "real-transport")]
pub mod remote_proxy;
#[cfg(feature = "real-transport")]
pub mod runtime_select;
#[cfg(feature = "real-transport")]
pub mod sidecar_reconcile;
#[cfg(feature = "real-transport")]
pub mod sidecar_store;
#[cfg(feature = "real-transport")]
pub mod sidecar_sweep;
// Shared helpers for the sidecar lifecycle test suites (never shipped;
// Linux-only like the suites themselves — they read `/proc` evidence).
#[cfg(all(test, target_os = "linux", feature = "real-transport"))]
pub(crate) mod sidecar_test_support;
#[cfg(feature = "real-transport")]
pub mod transport;

pub use app_server::{
Expand All @@ -72,6 +84,22 @@ pub use model::{
CodexEffortError, CHEAPEST_T2_MODEL, FRESHCODEX_DEFAULT_EFFORT, FRESHCODEX_DEFAULT_MODEL,
FRESHCODEX_EFFORTS_VERBATIM,
};
#[cfg(feature = "real-transport")]
pub use runtime_select::select_codex_runtime;
#[cfg(feature = "real-transport")]
pub use sidecar_reconcile::{
codex_sidecar_reconciler, set_codex_sidecar_reconciler, BootReconcileReport,
ReattachedCodexAppServerRuntime, SidecarReconciler,
};
#[cfg(feature = "real-transport")]
pub use sidecar_store::{
proc_cmdline, proc_starttime, set_codex_sidecar_store, verify_sidecar_identity,
CodexSidecarRecord, CodexSidecarStore, IdentityVerdict, SidecarRecordState,
SIDECAR_RECORD_VERSION,
};
#[cfg(feature = "real-transport")]
pub use sidecar_sweep::{kill_verified_sidecar_tree, KillOutcome, KillTreeOutcome};

pub use protocol::{
build_notification_frame, build_request_frame, classify_notification,
extract_turn_notification_event, parse_client_frame, parse_incoming_frame, turn_status,
Expand Down
40 changes: 40 additions & 0 deletions crates/freshell-codex/src/runtime_select.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! Plan-aware runtime selection (Task 7) — the reattach-vs-spawn seam the
//! production [`CodexRuntimeFactory`] dispatches through.
//!
//! Sibling of [`crate::sidecar_reconcile`] (the pre-authorized split: the
//! reconcile module sits at its 1,000-line ceiling): the reconciler owns the
//! CLAIM; this module owns the SELECTION the claim's outcome drives.
//!
//! [`CodexRuntimeFactory`]: crate::launch_lifecycle::CodexRuntimeFactory

use std::sync::Arc;

use crate::launch_lifecycle::{CodexLaunchRuntime, SpawnedCodexAppServerRuntime};
use crate::launch_plan::CodexLaunchPlan;
use crate::sidecar_reconcile::{ReattachedCodexAppServerRuntime, SidecarReconciler};
use crate::sidecar_store::CodexSidecarStore;

/// The production selection: a claimable verified survivor for the plan's
/// resume session ⇒ reattach; otherwise the spawn runtime. Reattach applies
/// only to resume plans (`plan.session_id` is `Some` ⇔ resume,
/// [`CodexLaunchPlan::session_id`]), so the A4 fresh-restore exclusion and
/// the 45s candidate-capture timer are untouched. `None` reconciler/store
/// (nothing installed at boot) ⇒ spawn — behavior identical to the
/// pre-reconciler world.
pub async fn select_codex_runtime(
reconciler: Option<&Arc<SidecarReconciler>>,
store: Option<&Arc<CodexSidecarStore>>,
plan: &CodexLaunchPlan,
) -> Arc<dyn CodexLaunchRuntime> {
if let (Some(reconciler), Some(store), Some(session_id)) =
(reconciler, store, plan.session_id.as_deref())
{
if let Some(record) = reconciler.claim_for_session(session_id).await {
return Arc::new(ReattachedCodexAppServerRuntime::new(
record,
Arc::clone(store),
));
}
}
Arc::new(SpawnedCodexAppServerRuntime::new())
}
Loading
Loading