feat(pi): publish observed harness state from the injected extension - #323
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 97aaa9c535
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
97aaa9c to
43f5213
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 43f5213560
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
43f5213 to
199fb59
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 199fb594f5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
199fb59 to
c2dde7f
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c2dde7ff52
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
c2dde7f to
8a7f2a1
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8a7f2a195b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
8a7f2a1 to
b60cdc9
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b60cdc977f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
b60cdc9 to
9a5f8c5
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9a5f8c5930
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
9a5f8c5 to
3e1f2e3
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3e1f2e3dc6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
3e1f2e3 to
41708c0
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 41708c0ca2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let session = harness_state::session_token(); | ||
| // The claim is written: it supersedes whatever the predecessor left — including a | ||
| // still-fresh live record — before the channel or terminal writer act under it. | ||
| let seq = harness_state::claim(&agent_dir, identity.clone(), "pi", &session)?; |
There was a problem hiding this comment.
Avoid publishing a terminal claim before pi is observed
When pi launches but its extension fails open—for example because ctx.isIdle is unavailable, the channel subprocess cannot start, or the protocol is rejected—nothing replaces this claim while the provider remains alive. harness_state::claim writes a fresh Ended record with reason superseded, and terminal records bypass the PTY liveness check, so roster and doctor consumers falsely report the running seat as ended for the 15-minute freshness horizon. Keep the takeover fence from being exposed as a definite terminal observation until the channel publishes state or the wrapper observes an actual exit.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 4a07c90 on schickling/hs-01-envelope (owning layer): readers derive indeterminate with the distinct reason 'claimed' from a fresh exitless superseded record -- the takeover placeholder is a fence, not an observation, so a live seat whose harness never publishes its first frame no longer reads dead for the freshness horizon; fencing, aging, and persistence are unchanged. The pi layer picks this up unchanged, and spec.md's projection table documents the row.
Posted on behalf of @schickling
| field | value |
|---|---|
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 |
The shipped extension emits observational state frames on agent_start/ agent_end (the exact span ctx.isIdle() covers) plus an idle-proof seed at session start. The channel owns the live record: it maps recognized state frames to observations, heartbeats exactly as long as its stdio connection lives, and on EOF leaves the record to age rather than asserting a state nobody watches. The session wrapper owns the one fact the channel cannot see and writes the terminal record with the provider's real exit status, via a new run_provider_observed that reports the exit instead of judging it. pi 0.84.2 exposes no waiting-on-a-human event, so no pi frame ever sets blockedOn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… sessions Review-pass fixes: idle emits on agent_settled instead of agent_end — measured against the repo's own pi captures, ctx.isIdle() is still false through agent_end and a queued follow-up turn starts exactly there, so the old edge blipped a spurious idle before it; the channel drops queued live frames once the wrapper's terminal record is on disk (the flock serializes but does not order two processes); and ptySession records the wrapper's runtime ID, delivered to the channel via ST2_PI_CHANNEL_RUNTIME_ID beside the existing three env vars. checks.pi-extension-types stays green against pinned pi, proving the settle event exists on that surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The channel writer starts interrupted, so a restarted seat opens a new transition instead of coalescing with — or being suppressed by — its predecessor's record, including a predecessor's terminal ended. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The extension attaches a stream-level stdin error listener so an EPIPE from a retiring channel can never take pi down; the wrapper mints the session incarnation token, exports it beside the other channel env, and both its terminal writer and the channel adopt it — the terminal record owns exactly this session's live records. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The extension stashes and unexports ALL ST2_PI_CHANNEL_* values — the runtime id and ownership pair included — handing them only to the channel subprocess, so no pi tool child inherits this seat's registry key or record ownership. The wrapper claims the ownership sequence at startup and exports it beside the token; the channel and the terminal writer act under the same directional claim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… its predecessor The wrapper's claim is a written supersession before the channel acts; the extension awaits (bounded) the previous channel's exit before spawning its replacement, so a predecessor cannot drain queued frames into the new session's records after the seed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ipped extension The cycle-5 bounded-await landed as a use-before-declaration — a TDZ ReferenceError on every channel open, so no managed pi seat got a channel at all — and the type gate shipped it green. The declaration is hoisted beside closeChild, and the gate now transpiles the real asset and drives it through registration, a double session_start (the exact region the regression lived in), the turn events, and shutdown; the injected-TDZ negative test fails the gate as required. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Found by the cycle-7 self-review's error-arm sweep: channel-env, hook-set verification, and extension-injection failures after the written claim left the placeholder as the last word. They now write a real terminal record (ended, launch-error) before propagating. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
41708c0 to
e0d4676
Compare
The pi producer — the first evented observed-state signal (the stronger signal DQ2 asks for), built on decision 0005's injected extension.
hooks/pi-channel.tsemits typed NDJSON state frames on pi's turn boundaries, with the idle edge onagent_settled— measured against the repo's own 2026-08-18 captures:ctx.isIdle()is still false throughagent_end, and a queued follow-up turn starts exactly at that boundary, so anagent_endidle emit would publish a spurious idle blip. Frames drop silently with no channel (fail-open).Ownership splits by phase: the channel owns the live record (it alone sees turn events; its stdio EOF bounds its evidence — heartbeats stop on EOF and the record ages out), the wrapper owns the terminal record with the real exit status. The channel drops queued live frames only against a terminal record its own session wrote — the incarnation's last word stands, while a predecessor's
endednever silences a restarted seat.ptySessionis the wrapper's runtime/task ID (ST2_PI_CHANNEL_RUNTIME_ID), the actual pty registry key — hand-authored seats with custom task IDs probe correctly.run_provider_observedreturns aProviderOutcomeso the wrapper can record how a session ended before judging the exit.pi 0.84.2 exposes no waiting-on-a-human event, so pi never sets
blockedOn: human— and therefore noaskkind — in v1; documented limit.checks.pi-extension-typesstays green against the pinned release.Cycle-2: a fresh channel is a new session — it starts interrupted, so its first frame opens a fresh transition instead of claiming continuity across the restart.
Cycle-3: the wrapper mints the session token (exported as ST2_PI_CHANNEL_SESSION) and passes a terminal-only observer, so the stop path's pre-escalation
endedwrite is real for pi; the extension attaches a stream-level stdin error listener so an EPIPE from a retiring channel can never take pi down.Cycle-4: the extension stashes and unexports every ST2_PI_CHANNEL_* value (runtime id + ownership pair included), handing them only to the channel subprocess; the wrapper claims and exports the ownership sequence beside its token.
Cycle-6 (P1): the cycle-5 bounded-await landed as a use-before-declaration — a TDZ ReferenceError on every channel open, so no managed pi seat got a channel — and the type gate shipped it green. The declaration is hoisted, and the gate now transpiles and runtime-smokes the shipped asset (registration, double session_start, turn events, shutdown), with an injected-TDZ negative test proving the gate can fail.
Part of the #268 stack (#319→#326).
🤖 Generated with Claude Code