Skip to content

Commit 4a07c90

Browse files
fix(harness-state): a fresh claim placeholder reads indeterminate, never definite ended
The claim placeholder is a fence, not an observation: the session wrote it at startup and has observed nothing yet. A live seat whose harness never publishes its first frame promptly — pi's extension failing open, for one — read as dead for the whole freshness horizon while its process ran. Readers now derive indeterminate with the distinct reason 'claimed' from a fresh exitless superseded record; fencing, aging, and persistence are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> agent-identity: unknown agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.0.3 agent-runtime: OMP 18.0.3 tooling-profile: dotfiles@f33cd9c-dirty
1 parent a8d5915 commit 4a07c90

1 file changed

Lines changed: 25 additions & 6 deletions

File tree

src/harness_state.rs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,16 @@ fn read_raw_at(
614614
// A literal `unknown` is never written by this crate; treat one like malformation.
615615
return Observed::indeterminate("literal-unknown", harness);
616616
}
617+
if record.state == Activity::Ended
618+
&& record.exit.is_none()
619+
&& record.reason.as_deref() == Some("superseded")
620+
{
621+
// The claim placeholder is a fence, not an observation: the session wrote it at startup
622+
// and has observed nothing yet. Reading it as definite `ended` would flip a live seat
623+
// to dead for every consumer whose harness never publishes its first frame promptly —
624+
// indeterminate, distinctly, until the first real observation or the ordinary horizon.
625+
return Observed::indeterminate("claimed", harness);
626+
}
617627
if record.state != Activity::Ended
618628
&& let Some(probe) = probe
619629
{
@@ -709,8 +719,9 @@ fn next_stamp(on_disk: Option<&Record>, now_ms: u64) -> u64 {
709719
/// returned for the wrapper to adopt and export beside its token. Writing the claim makes it
710720
/// atomic: racing claimers serialize on the lock and mint DISTINCT sequences, and a
711721
/// predecessor's fresh live record is superseded at relaunch even though the pty-name-based
712-
/// probe cannot tell the sessions apart. The record reads `ended (superseded)` until the
713-
/// session's first real observation replaces it.
722+
/// probe cannot tell the sessions apart. Readers derive indeterminate (`claimed`) from the
723+
/// fresh placeholder — a fence, not an observation — until the session's first real
724+
/// observation replaces it.
714725
pub fn claim(
715726
agent_dir: &Path,
716727
agent: impl Into<String>,
@@ -1272,7 +1283,11 @@ mod tests {
12721283
stale_bytes,
12731284
"the written claim itself supersedes the predecessor"
12741285
);
1275-
assert_eq!(read(&path, None).unwrap().state, Activity::Ended);
1286+
assert_eq!(
1287+
read(&path, None).unwrap().reason.as_deref(),
1288+
Some("claimed"),
1289+
"the fresh placeholder reads indeterminate, never definite ended"
1290+
);
12761291

12771292
// Once this session observes something, heartbeats re-stamp again.
12781293
writer.observe(active()).unwrap();
@@ -1667,7 +1682,7 @@ mod tests {
16671682

16681683
/// T3: at relaunch the claim supersedes the predecessor's still-fresh live record — the
16691684
/// pty-name-based probe cannot tell the sessions apart, so the record itself must — and the
1670-
/// seat reads `ended (superseded)` until the new session's first real observation.
1685+
/// seat reads indeterminate (`claimed`) until the new session's first real observation.
16711686
#[test]
16721687
fn a_relaunch_claim_supersedes_a_fresh_live_predecessor_record() {
16731688
let tmp = tempfile::tempdir().unwrap();
@@ -1677,8 +1692,12 @@ mod tests {
16771692
let token = session_token();
16781693
let seq = claim(tmp.path(), "hetz.worker", "codex", &token).unwrap();
16791694
let observed = read(&path, None).unwrap();
1680-
assert_eq!(observed.state, Activity::Ended);
1681-
assert_eq!(observed.reason.as_deref(), Some("superseded"));
1695+
assert_eq!(
1696+
observed.state,
1697+
Activity::Unknown,
1698+
"the fresh placeholder is a fence, not a definite ended"
1699+
);
1700+
assert_eq!(observed.reason.as_deref(), Some("claimed"));
16821701
assert_eq!(observed.exit, None);
16831702

16841703
let mut successor = Writer::new(tmp.path(), "hetz.worker", "codex", Some("worker".into()))

0 commit comments

Comments
 (0)