mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 06:59:42 +02:00
fix(respawn): do not revive a stopped controller after a cycle-step write
Each cycle step (kickstart, update, /clear, /init) checks for `stopped` before
`await session.writeViaMux(...)`, then emits `stepSent` and calls
`setState('waiting_*')` after it.
stop() is asynchronous with respect to that await. One that lands while the
write is in flight has already passed the guard that ran, so the post-await
setState() puts a stopped controller back into a waiting state — re-arming its
step timers against a session the user asked to stop.
Re-check after the await, before emitting and setting state.
The guard reads the public `state` getter rather than `_state` on purpose:
TypeScript narrows `_state` across the await from the pre-await check and cannot
see that stop() mutated it, so `this._state === 'stopped'` is rejected as a
comparison with no overlap (TS2367) at all four sites.
Adds test/respawn-stop-race.test.ts, which drives the interleaving
deterministically by calling stop() from inside the mocked write rather than
relying on timing. All four steps go red without these guards.
This commit is contained in:
@@ -1624,6 +1624,11 @@ export class RespawnController extends EventEmitter {
|
||||
const prompt = this.config.kickstartPrompt!;
|
||||
this.logAction('command', `Sending kickstart: "${prompt.substring(0, 40)}..."`);
|
||||
await this.session.writeViaMux(prompt + '\r'); // \r triggers key.return in Ink/Claude CLI
|
||||
// COD-51: stop() may have run during the await; re-check before reviving the
|
||||
// state machine. Reads the public getter, not `_state`: TypeScript narrows
|
||||
// `_state` across the await from the guard above and cannot see that stop()
|
||||
// mutated it, so the comparison would be flagged as impossible.
|
||||
if (this.state === 'stopped') return;
|
||||
this.emit('stepSent', 'kickstart', prompt);
|
||||
this.setState('waiting_kickstart');
|
||||
this.promptDetected = false;
|
||||
@@ -2833,6 +2838,11 @@ export class RespawnController extends EventEmitter {
|
||||
const input = updatePrompt + '\r'; // \r triggers Enter in Ink/Claude CLI
|
||||
this.logAction('command', `Sending: "${updatePrompt.substring(0, 50)}..."`);
|
||||
await this.session.writeViaMux(input);
|
||||
// COD-51: stop() may have run during the await; re-check before reviving the
|
||||
// state machine. Reads the public getter, not `_state`: TypeScript narrows
|
||||
// `_state` across the await from the guard above and cannot see that stop()
|
||||
// mutated it, so the comparison would be flagged as impossible.
|
||||
if (this.state === 'stopped') return;
|
||||
this.emit('stepSent', 'update', updatePrompt);
|
||||
this.setState('waiting_update');
|
||||
this.promptDetected = false;
|
||||
@@ -2860,6 +2870,11 @@ export class RespawnController extends EventEmitter {
|
||||
if (this._state === 'stopped') return;
|
||||
this.logAction('command', 'Sending: /clear');
|
||||
await this.session.writeViaMux('/clear\r'); // \r triggers Enter in Ink/Claude CLI
|
||||
// COD-51: stop() may have run during the await; re-check before reviving the
|
||||
// state machine. Reads the public getter, not `_state`: TypeScript narrows
|
||||
// `_state` across the await from the guard above and cannot see that stop()
|
||||
// mutated it, so the comparison would be flagged as impossible.
|
||||
if (this.state === 'stopped') return;
|
||||
this.emit('stepSent', 'clear', '/clear');
|
||||
this.setState('waiting_clear');
|
||||
this.promptDetected = false;
|
||||
@@ -2902,6 +2917,11 @@ export class RespawnController extends EventEmitter {
|
||||
if (this._state === 'stopped') return;
|
||||
this.logAction('command', 'Sending: /init');
|
||||
await this.session.writeViaMux('/init\r'); // \r triggers Enter in Ink/Claude CLI
|
||||
// COD-51: stop() may have run during the await; re-check before reviving the
|
||||
// state machine. Reads the public getter, not `_state`: TypeScript narrows
|
||||
// `_state` across the await from the guard above and cannot see that stop()
|
||||
// mutated it, so the comparison would be flagged as impossible.
|
||||
if (this.state === 'stopped') return;
|
||||
this.emit('stepSent', 'init', '/init');
|
||||
this.setState('waiting_init');
|
||||
this.promptDetected = false;
|
||||
|
||||
Reference in New Issue
Block a user