Skip to content

Commit b60cdc9

Browse files
schicklingclaude
andcommitted
fix(pi): written ownership claim, and the successor channel waits out 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>
1 parent 90d3e80 commit b60cdc9

2 files changed

Lines changed: 21 additions & 4 deletions

File tree

hooks/pi-channel.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export default function (pi: ExtensionAPI) {
111111
};
112112

113113
/** Open a channel and resolve with the hello's restored context (empty if none, or on timeout). */
114-
const open = (ctx: ExtensionContext): Promise<string> => {
114+
const open = async (ctx: ExtensionContext): Promise<string> => {
115115
if (!bin || !catalog || !identity) return Promise.resolve("");
116116
if (typeof ctx.isIdle !== "function") {
117117
// Refuse rather than degrade. Without a positive idle proof this extension cannot choose
@@ -124,9 +124,24 @@ export default function (pi: ExtensionAPI) {
124124
);
125125
return Promise.resolve("");
126126
}
127-
// Closes the channel opened by the PREVIOUS session, whichever extension instance opened it.
128-
closeChild(state.child);
127+
// Closes the channel opened by the PREVIOUS session, whichever extension instance opened
128+
// it — and WAITS (bounded) for it to exit before the replacement spawns: the successor
129+
// shares the seat's record, and a predecessor draining its queued frames after the new
130+
// session's seed would land stale state into fresh records.
131+
const previous = state.child;
132+
closeChild(previous);
133+
await awaitExit(previous, 2000);
129134

135+
const awaitExit = (child: childProcess.ChildProcess | undefined, ms: number) =>
136+
new Promise<void>((resolve) => {
137+
if (!child || child.exitCode !== null || child.signalCode !== null) return resolve();
138+
const timer = setTimeout(resolve, ms);
139+
timer.unref?.();
140+
child.once("exit", () => {
141+
clearTimeout(timer);
142+
resolve();
143+
});
144+
});
130145
const channelEnv: NodeJS.ProcessEnv = { ...process.env };
131146
if (runtimeId) channelEnv[RUNTIME_ID] = runtimeId;
132147
if (session) channelEnv[SESSION] = session;

src/pi_session.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ pub fn run(
6464
let executable =
6565
std::env::current_exe().context("resolving st2 executable for the pi channel")?;
6666
let session = harness_state::session_token();
67-
let seq = harness_state::claim_seq(&agent_dir);
67+
// The claim is written: it supersedes whatever the predecessor left — including a
68+
// still-fresh live record — before the channel or terminal writer act under it.
69+
let seq = harness_state::claim(&agent_dir, identity.clone(), "pi", &session)?;
6870
let mut env = channel_env(
6971
&executable,
7072
catalog_root,

0 commit comments

Comments
 (0)