Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
2b36188
docs: add implementation plan for reconcile-client-adoption
danshapiro Jul 26, 2026
c8a2d93
docs: add implementation plan for freshagent-verdicts-resume
danshapiro Jul 26, 2026
e0a5617
docs: add implementation plan for recover-my-panes
danshapiro Jul 26, 2026
5319d75
docs: add implementation plan for codex-rollout-locator
danshapiro Jul 26, 2026
0303db5
docs(plan): fix reconcile-client-adoption plan per load-bearing assum…
danshapiro Jul 26, 2026
c13ee24
docs: harden recover-my-panes plan after load-bearing assumption vali…
danshapiro Jul 26, 2026
881a64c
docs(plan): harden codex-rollout-locator plan per load-bearing valida…
danshapiro Jul 26, 2026
f9edd5d
docs: fix Task 3 import-order invariant to match actual main.tsx imports
danshapiro Jul 26, 2026
75d99a8
docs(plan): harden B4 freshagent-verdicts-resume plan with load-beari…
danshapiro Jul 26, 2026
cc0444c
feat(ws): delete retry verdict; bounded index-warming deferral yields…
danshapiro Jul 26, 2026
be3cf32
feat(ws): provider_unavailable existence state yields immediate hones…
danshapiro Jul 26, 2026
6c3e32c
docs(plan): fix fresheyes iteration-1 blocking issues in B4 plan
danshapiro Jul 26, 2026
b988e8f
docs(plan): fix fresh-eyes iteration 1 blocking issues in codex-rollo…
danshapiro Jul 26, 2026
ee9374d
feat(ws): sessionRef-level attach in verdict derivation + duplicate-P…
danshapiro Jul 26, 2026
264549b
docs: fix fresheyes iteration-2 blockers in recover-my-panes plan
danshapiro Jul 26, 2026
dda221d
fix(ws): legacy resumeSessionId claims join the sessionRef-attach bra…
danshapiro Jul 26, 2026
ee74b2e
docs(plan): register new test modules in Step 1 so TDD RED gates actu…
danshapiro Jul 26, 2026
691f204
docs(plan): fix fresheyes iteration-2 blocking issues in B4 plan
danshapiro Jul 26, 2026
99e734e
docs: fix fresheyes iteration-3 blockers in recover-my-panes plan
danshapiro Jul 26, 2026
bf45f7c
feat(terminal): sessionRef liveness-bound lease with TTL kill-before-…
danshapiro Jul 26, 2026
45b0bc3
docs(plan): make Task 5 hijack-guard test non-vacuous per fresh-eyes …
danshapiro Jul 26, 2026
16c00cb
fix(terminal): prune sessionRef bindings on row removal; make complet…
danshapiro Jul 26, 2026
d96d71f
docs(plan): fix fresheyes iteration-3 blocking issue in B4 plan
danshapiro Jul 26, 2026
b8cb904
feat(server): pure recovery-inventory builder joining snapshots with …
danshapiro Jul 26, 2026
c01b0b2
feat(sessions): CodexLocator skeleton - arm gates, fs-snapshot substr…
danshapiro Jul 26, 2026
5745022
feat(ledger): resume-invocation record fields + fresh-agent binding u…
danshapiro Jul 26, 2026
bcbf8ae
test(sessions): pin CodexLocator window semantics and correlation guards
danshapiro Jul 26, 2026
00307ff
feat(server): GET /api/recovery/inventory - device-scoped recoverable…
danshapiro Jul 26, 2026
7d19861
test(sessions): restore newline-terminated event_msg line so the Prob…
danshapiro Jul 26, 2026
14ecfed
test(sessions): write valid wrong-type JSON in Never-branch pin (rest…
danshapiro Jul 26, 2026
043a934
feat(ws): refuse sessionRef restore while a Running terminal owns the…
danshapiro Jul 26, 2026
fdf1c45
chore: remove stray editor backup file committed by mistake
danshapiro Jul 26, 2026
65d6662
feat(freshagent): PaneIdentitySink trait - crate-boundary bridge for …
danshapiro Jul 26, 2026
88a3393
feat(ws): per-sessionRef single-flight on the create path with SESSIO…
danshapiro Jul 26, 2026
0f2c342
feat(client): recovery trigger eligibility (boot-captured) and dismis…
danshapiro Jul 26, 2026
64af488
feat(server): wire pane ledger into fresh-agent states via LedgerIden…
danshapiro Jul 26, 2026
c8d5924
refactor(ws): extract adopt_codex_identity shared adoption tail from …
danshapiro Jul 26, 2026
4b0209f
feat(client): recovery inventory types, API helper, and recovery-plan…
danshapiro Jul 26, 2026
2610252
feat(protocol): pane.reconcile client schemas, ready capabilities, re…
danshapiro Jul 26, 2026
18a9469
refactor(ws): delete duplicated guard 3b from candidate handler - ado…
danshapiro Jul 26, 2026
ec4324e
feat(codex): write fresh-agent ledger binding rows at every identity …
danshapiro Jul 26, 2026
f4322f8
feat(client): self-gating recover-my-panes offer panel with accept/de…
danshapiro Jul 26, 2026
5cfcb93
feat(ws): codex locator controller + arm/submit/disarm seams + 150ms …
danshapiro Jul 26, 2026
07ecbb3
feat(store): reconcile verdict fold reducers - attach/reset without c…
danshapiro Jul 26, 2026
a1f7817
fix(client): port confirm-modal focus pattern to RecoveryOfferPanel; …
danshapiro Jul 26, 2026
2eec577
fix(codex): resume paths reapply model/sandbox/permission/effort from…
danshapiro Jul 26, 2026
642a6c7
test(ws): codex drain_and_associate binds identity+ledger+broadcasts;…
danshapiro Jul 26, 2026
3583a1f
feat(codex): user-visible THREAD_MEMORY_LOST degradation frame on cra…
danshapiro Jul 26, 2026
8e7279f
test(ws): fresh codex pane turn.complete carries sessionId via server…
danshapiro Jul 26, 2026
7ac12ec
feat(client): pane-reconcile module - request builder, verdict foldin…
danshapiro Jul 26, 2026
567f576
feat(client): mount recovery offer panel in App (Lane B3 region) (B3/…
danshapiro Jul 26, 2026
37fe753
feat(opencode): pending marker at create, binding row at ses_* materi…
danshapiro Jul 26, 2026
63b6b79
feat(client): advertise paneReconcileV1, surface ready capabilities, …
danshapiro Jul 26, 2026
4767b7e
feat(ws)!: retire terminal.codex.candidate.persisted writer - accept-…
danshapiro Jul 26, 2026
78a57da
test: stabilize recover-my-panes unit/integration coverage (B3)
danshapiro Jul 26, 2026
f31b9c0
test(e2e): fake codex terminal fixture that writes a real gated rollo…
danshapiro Jul 26, 2026
a5ce3c6
fix(opencode): resume reapplies model/effort/cwd from the ledger reco…
danshapiro Jul 26, 2026
8d396fd
feat(client): reconcile on ready-with-capability; census becomes the …
danshapiro Jul 26, 2026
d9e413f
feat(claude): record fresh-agent binding row with create settings at …
danshapiro Jul 26, 2026
4a77940
test(e2e): fresh codex pane gains server-side identity and resumes vi…
danshapiro Jul 26, 2026
c64b4c3
fix(claude): resume-in-place reapplies model/permissionMode/effort fr…
danshapiro Jul 26, 2026
41b5013
test(e2e): codex SIGKILL-inside-locator-window leaves durable fresh-b…
danshapiro Jul 26, 2026
557749e
feat(client): verdict-driven create args; bounded SESSION_RESERVED/IN…
danshapiro Jul 26, 2026
ad33857
feat(ws): paneReconcileFreshAgentV1 capability - parsed, threaded, ec…
danshapiro Jul 26, 2026
d1c59bb
test(e2e): browser-loss recover-my-panes proof - offer, accept-resume…
danshapiro Jul 26, 2026
6211336
feat(ws): reconcile_freshagent - pure fresh-agent verdict mapping wit…
danshapiro Jul 26, 2026
6b3a163
feat(client): batched dead-session adjudication panel + single warmin…
danshapiro Jul 26, 2026
9962e0f
test(e2e): remove offer retry loop - fresh-context offer visibility i…
danshapiro Jul 26, 2026
786153d
feat(ws): fresh-agent panes enter the reconcile verdict system behind…
danshapiro Jul 26, 2026
2ed6b94
refactor(server): delete operator tabs-sync restore endpoint, write-a…
danshapiro Jul 26, 2026
10aaa72
test(e2e): reconcile client adoption proofs; flip P1.7 D8 pin - two c…
danshapiro Jul 26, 2026
1440d13
test(e2e): per-provider settings survive restart + codex crash memory…
danshapiro Jul 26, 2026
db236ec
test(wall): flip/narrow P1.13 freshopencode settings pin - fresh-agen…
danshapiro Jul 26, 2026
b2ce5cb
fix(review): claude all-blank binding guard, shared retire-and-link h…
danshapiro Jul 26, 2026
2571097
fix(review): complete retire-and-link helper extraction at the fresh-…
danshapiro Jul 26, 2026
402e3ed
test(e2e): fixture realism + adopted-reality fixes for 7 reconcile ga…
danshapiro Jul 26, 2026
3918fbc
fix: final-review findings - claim-side TOCTOU re-check + App fold cl…
danshapiro Jul 26, 2026
413dd22
merge: B1 feat/reconcile-client-adoption into waveB integration
danshapiro Jul 26, 2026
2cd9a12
merge: B2 feat/codex-rollout-locator into waveB integration
danshapiro Jul 26, 2026
a66ed62
merge: B3 feat/recover-my-panes into waveB integration
danshapiro Jul 26, 2026
f476f89
merge: B4 feat/freshagent-verdicts-resume into waveB integration
danshapiro Jul 26, 2026
c2eb82d
fix: cross-lane compile fallout from waveB merges (designed B1xB4 seams)
danshapiro Jul 26, 2026
fc6d909
fix: B1xB3 test seam - App reconcile-adoption mock gains getRecoveryI…
danshapiro Jul 26, 2026
6e59828
fix: B1xB2 e2e seam - codex restore spec polls the pane's CURRENT ter…
danshapiro Jul 26, 2026
8d1f21b
test: narrow wave-A A2xA3 ledger pin for B4's kind:fresh-agent rows
danshapiro Jul 26, 2026
6f27336
test(ws): pin B1xB4 seams - single-burn across warming deferral + Pro…
danshapiro Jul 26, 2026
857c9d4
fix(ws): B2xB4 misbind hardening - codex adoption refuses freshagent-…
danshapiro Jul 26, 2026
143bfd2
docs: B3xB4 - mark freshopencode fresh-start limitation RESOLVED at w…
danshapiro Jul 26, 2026
2b3b4fa
fix(freshagent): no-laundering guard on the REST send-keys binding wr…
danshapiro Jul 26, 2026
acf8fe9
fix(pwa): first SW claim never reloads - closes B3's first-boot offer…
danshapiro Jul 26, 2026
d9b71f5
fix(server): widen recovery-inventory D7 liveness join to match the c…
danshapiro Jul 26, 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
1 change: 1 addition & 0 deletions Cargo.lock

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

402 changes: 395 additions & 7 deletions crates/freshell-freshagent/src/claude.rs

Large diffs are not rendered by default.

665 changes: 632 additions & 33 deletions crates/freshell-freshagent/src/codex.rs

Large diffs are not rendered by default.

197 changes: 197 additions & 0 deletions crates/freshell-freshagent/src/identity_sink.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
//! P1.13 crate-boundary bridge: fresh-agent identity events flow OUT of this
//! crate through this trait; `freshell-server` implements it over the pane
//! ledger (this crate must not depend on `freshell-ws`, where the ledger
//! lives — the dependency edge runs the other way).

use std::sync::Arc;

/// Resume-invocation record (campaign plan §4.2): exactly what the
/// provider-native resume command needs.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct FreshAgentSettings {
pub model: Option<String>,
pub sandbox: Option<String>,
pub permission_mode: Option<String>,
pub effort: Option<String>,
pub cwd: Option<String>,
}

/// One fresh-agent identity event. Settings are a FULL snapshot (replace,
/// not merge). `resolves_pending` names a pending marker (placeholder id)
/// this binding supersedes.
#[derive(Debug, Clone, PartialEq)]
pub struct FreshAgentBindingUpsert {
pub provider: String,
pub session_id: String,
pub mode: String,
pub create_request_id: Option<String>,
pub resolves_pending: Option<String>,
/// G3 supersession (V8/A14): OLD session id this binding replaces
/// (codex crash-respawn passes the old thread id; everyone else None).
pub supersedes: Option<String>,
pub settings: FreshAgentSettings,
}

/// Write-completion future (see Interfaces block for the style citation:
/// BoxFuture aliases at freshell-opencode/src/serve.rs:44 /
/// freshell-codex/src/app_server.rs:62; no async-trait dep in the workspace).
pub type SinkWrite =
std::pin::Pin<Box<dyn std::future::Future<Output = std::io::Result<()>> + Send + 'static>>;

/// AWAITED writes (wave-A durable-before-answer policy, V8/A11): callers
/// `.await` the returned future before replying/broadcasting/proceeding.
/// Implementations run fsync work on `spawn_blocking` and propagate failures
/// as `Err` — call sites surface them user-visibly, then proceed (a write
/// failure never blocks the identity event). Reads are memory-fast + sync.
pub trait PaneIdentitySink: Send + Sync {
fn record_pending(&self, placeholder_id: &str, mode: &str, cwd: Option<&str>) -> SinkWrite;
fn record_binding(&self, upsert: FreshAgentBindingUpsert) -> SinkWrite;
fn load_settings(&self, provider: &str, session_id: &str) -> Option<FreshAgentSettings>;
/// True iff a fresh-agent binding row was EVER recorded for this key —
/// the SETTINGS_RESET alarm gate (V7/A10): alarm only when the ledger
/// proves prior recording; never for never-recorded sessions.
fn was_recorded(&self, provider: &str, session_id: &str) -> bool;
}

pub type SharedPaneIdentitySink = Arc<dyn PaneIdentitySink>;

/// In-memory sink for tests, crate-wide. Mutations happen synchronously
/// before the (already-completed) future is returned, so tests can assert
/// immediately after `.await`.
#[cfg(test)]
#[derive(Default)]
pub(crate) struct FakeIdentitySink {
pub pendings: std::sync::Mutex<Vec<(String, String, Option<String>)>>,
pub bindings: std::sync::Mutex<Vec<FreshAgentBindingUpsert>>,
pub settings: std::sync::Mutex<std::collections::HashMap<(String, String), FreshAgentSettings>>,
/// Keys ever recorded (or seeded) — backs `was_recorded`.
pub recorded: std::sync::Mutex<std::collections::HashSet<(String, String)>>,
/// When true, write futures resolve to Err — for failure-surfacing tests.
pub fail_writes: std::sync::atomic::AtomicBool,
}

#[cfg(test)]
impl FakeIdentitySink {
#[allow(dead_code)] // used by identity-event tasks (Tasks 4-10 tests)
pub fn seed(&self, provider: &str, session_id: &str, s: FreshAgentSettings) {
self.recorded
.lock()
.unwrap()
.insert((provider.into(), session_id.into()));
self.settings
.lock()
.unwrap()
.insert((provider.into(), session_id.into()), s);
}
/// Mark a key as previously recorded WITHOUT a recoverable snapshot —
/// the SETTINGS_RESET-alarm-positive fixture (V7/A10 gating).
#[allow(dead_code)] // used by identity-event tasks (Tasks 4-10 tests)
pub fn seed_recorded_only(&self, provider: &str, session_id: &str) {
self.recorded
.lock()
.unwrap()
.insert((provider.into(), session_id.into()));
}
fn write_result(&self) -> SinkWrite {
if self.fail_writes.load(std::sync::atomic::Ordering::SeqCst) {
Box::pin(std::future::ready(Err(std::io::Error::other(
"fake write failure",
))))
} else {
Box::pin(std::future::ready(Ok(())))
}
}
}

#[cfg(test)]
impl PaneIdentitySink for FakeIdentitySink {
fn record_pending(&self, placeholder_id: &str, mode: &str, cwd: Option<&str>) -> SinkWrite {
if !self.fail_writes.load(std::sync::atomic::Ordering::SeqCst) {
self.pendings.lock().unwrap().push((
placeholder_id.into(),
mode.into(),
cwd.map(Into::into),
));
}
self.write_result()
}
fn record_binding(&self, upsert: FreshAgentBindingUpsert) -> SinkWrite {
if !self.fail_writes.load(std::sync::atomic::Ordering::SeqCst) {
self.recorded
.lock()
.unwrap()
.insert((upsert.provider.clone(), upsert.session_id.clone()));
self.settings.lock().unwrap().insert(
(upsert.provider.clone(), upsert.session_id.clone()),
upsert.settings.clone(),
);
self.bindings.lock().unwrap().push(upsert);
}
self.write_result()
}
fn load_settings(&self, provider: &str, session_id: &str) -> Option<FreshAgentSettings> {
self.settings
.lock()
.unwrap()
.get(&(provider.into(), session_id.into()))
.cloned()
}
fn was_recorded(&self, provider: &str, session_id: &str) -> bool {
self.recorded
.lock()
.unwrap()
.contains(&(provider.into(), session_id.into()))
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;

#[tokio::test]
async fn fake_sink_records_and_serves_settings() {
let fake = Arc::new(FakeIdentitySink::default());
fake.record_pending("freshopencode-r1", "freshopencode", Some("/w"))
.await
.expect("pending write ok");
fake.record_binding(FreshAgentBindingUpsert {
provider: "opencode".into(),
session_id: "ses_1".into(),
mode: "freshopencode".into(),
create_request_id: Some("r1".into()),
resolves_pending: Some("freshopencode-r1".into()),
supersedes: None,
settings: FreshAgentSettings {
model: Some("m".into()),
sandbox: None,
permission_mode: None,
effort: Some("low".into()),
cwd: Some("/w".into()),
},
})
.await
.expect("binding write ok");
let s = fake.load_settings("opencode", "ses_1").expect("settings");
assert_eq!(s.model.as_deref(), Some("m"));
assert_eq!(s.effort.as_deref(), Some("low"));
assert_eq!(fake.pendings.lock().unwrap().len(), 1);
assert_eq!(fake.bindings.lock().unwrap().len(), 1);
assert!(fake.load_settings("opencode", "nope").is_none());
assert!(fake.was_recorded("opencode", "ses_1"));
assert!(!fake.was_recorded("opencode", "nope"));
}

#[tokio::test]
async fn fake_sink_failure_knob_returns_err() {
let fake = Arc::new(FakeIdentitySink::default());
fake.fail_writes
.store(true, std::sync::atomic::Ordering::SeqCst);
assert!(
fake.record_pending("p", "freshopencode", None)
.await
.is_err(),
"failure must surface as Err, never be swallowed"
);
}
}
Loading
Loading