@excitedjs/agent-runtime-claude-code 0.6.1 → 0.7.0-alpha.g13b5c64f637b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/rpc.js CHANGED
@@ -1,290 +1,154 @@
1
- /**
2
- * Claude Code stream-json turn RPC.
3
- *
4
- * The supervisor owns the child process. This class owns one in-flight command
5
- * group, stdout line demux, command drainage, and defensive control replies.
6
- *
7
- * One resident CLI execution window can span several submitted commands: a
8
- * live steer is written while the CLI is already running. How the CLI answers them is
9
- * not fixed, which is what makes settlement subtle (probed against a live
10
- * 2.1.231 resident session):
11
- *
12
- * - **Commands fold.** A message that arrives while the in-flight turn is
13
- * inside a tool call is absorbed into that turn at the next query-loop
14
- * boundary. Several commands then share ONE `result` (3 → 1 observed), and
15
- * a folded command's uuid never appears on any `result`.
16
- * - **Or they do not.** A command that arrives between turns runs on its own
17
- * and gets its own `result`.
18
- * - **`result.user_message_uuid` is not a completion ledger.** It is present
19
- * only sometimes, is not reliably the first-submitted uuid of a fold, and
20
- * is absent entirely on the `error_during_execution` artifact an
21
- * interrupt produces.
22
- * - **`command_lifecycle` is the attribution signal.** Started commands identify
23
- * the submissions represented by the next native `result`; terminal states
24
- * drain the resident execution window. Its ordering against `result` is not
25
- * stable.
26
- *
27
- * Every valid `result` is forwarded immediately as its own native completion
28
- * boundary. Lifecycle terminality only decides when the command group has
29
- * drained and the resident session may accept a new initial command; it never
30
- * aggregates several results into one completion.
31
- */
32
1
  import { randomUUID } from 'node:crypto';
33
2
  import { buildCanUseToolAllow, buildControlAck, buildRemoteControlEnable, buildUserMessage, LineBuffer, parseLine, TurnAggregator, } from './stream.js';
34
- /** Provider-private admission classification; never crosses the neutral API. */
35
- export class ClaudeSteerAdmissionError extends Error {
36
- admission;
37
- constructor(admission, message, options) {
38
- super(message, options);
39
- this.admission = admission;
40
- this.name = 'ClaudeSteerAdmissionError';
41
- }
42
- }
3
+ import { completionFromTurnOutcome } from './runtime-session.js';
4
+ /**
5
+ * One resident transport, one association from UUID to unanswered request.
6
+ * Native results consume requests directly; no aggregate execution window waits
7
+ * for terminal lifecycle frames. The consumed set also includes native internal
8
+ * commands so cancellation is scoped to work that actually entered a turn.
9
+ */
43
10
  export class ClaudeCodeStreamRpc {
44
11
  stdin;
45
12
  options;
46
13
  lineBuf = new LineBuffer();
47
- pending = null;
14
+ aggregator = new TurnAggregator();
15
+ requests = new Map();
16
+ consumed = new Set();
48
17
  lifecycleSupported = null;
18
+ timer = null;
19
+ closed = false;
49
20
  remoteControlRequestId = null;
50
21
  constructor(stdin, options) {
51
22
  this.stdin = stdin;
52
23
  this.options = options;
53
24
  }
54
- async submitTurn(prompt, options = {}, commandUuid = randomUUID()) {
55
- if (!this.stdin.writable) {
56
- return Promise.reject(new Error('claude resident child is not running'));
25
+ submit(prompt, options = {}, commandUuid = randomUUID()) {
26
+ if (this.closed || !this.stdin.writable) {
27
+ return Promise.resolve({ status: 'failed', error: new Error('claude resident child is not running') });
57
28
  }
58
- if (this.pending !== null) {
59
- return Promise.reject(new Error('claude resident session is already mid-turn'));
29
+ const concurrent = this.requests.size > 0;
30
+ if (concurrent && this.lifecycleSupported === false) {
31
+ return Promise.resolve({ status: 'failed', error: lifecycleUnsupportedError() });
60
32
  }
61
- return new Promise((resolve, reject) => {
62
- const pending = {
63
- resolve,
64
- reject,
65
- aggregator: new TurnAggregator(),
66
- timer: null,
67
- submitted: [commandUuid],
68
- terminal: new Set(),
69
- startedSinceResult: new Set(),
70
- ranAnyCommand: false,
71
- sawResult: false,
72
- lastAbnormalReason: null,
73
- capabilityWaiters: [],
74
- writeWaiters: new Map(),
75
- };
76
- this.pending = pending;
77
- // Arm the idle deadline (reset on every inbound stream line in `onLine`).
78
- this.armIdleTimer(pending);
79
- try {
80
- this.stdin.write(`${buildUserMessage(prompt, options, commandUuid)}\n`, (err) => {
81
- if (err != null && this.pending === pending) {
82
- const error = asError(err);
83
- this.settlePending(error)?.reject(error);
33
+ let settle;
34
+ const submission = Object.freeze({
35
+ settled: new Promise((resolve) => { settle = resolve; }),
36
+ });
37
+ return new Promise((admit) => {
38
+ const request = {
39
+ submission,
40
+ settle,
41
+ admit,
42
+ write: () => {
43
+ request.write = null;
44
+ this.armIdleTimer();
45
+ try {
46
+ this.stdin.write(`${buildUserMessage(prompt, options, commandUuid)}\n`, (error) => {
47
+ if (error != null)
48
+ this.failWrite(commandUuid, request, error);
49
+ else
50
+ this.acceptRequest(request);
51
+ });
84
52
  }
85
- });
86
- }
87
- catch (error) {
88
- if (this.pending === pending) {
89
- const failure = asError(error);
90
- this.settlePending(failure)?.reject(failure);
91
- }
53
+ catch (error) {
54
+ this.failWrite(commandUuid, request, asError(error));
55
+ }
56
+ },
57
+ };
58
+ // Registration precedes writing: a transport may synchronously acknowledge
59
+ // or answer this message before write() returns or invokes its callback.
60
+ this.requests.set(commandUuid, request);
61
+ if (this.lifecycleSupported === true) {
62
+ // Reentrant input must not overtake older requests waiting on init.
63
+ for (const pending of this.requests.values())
64
+ pending.write?.();
92
65
  }
66
+ else if (!concurrent)
67
+ request.write();
93
68
  });
94
69
  }
95
- async steerTurn(prompt, options = {}, commandUuid = randomUUID()) {
96
- if (!this.stdin.writable) {
97
- return Promise.reject(preAdmissionError('claude resident child is not running'));
98
- }
99
- if (this.pending === null) {
100
- return Promise.reject(preAdmissionError('claude resident session has no active turn'));
101
- }
102
- const pending = this.pending;
103
- if (this.lifecycleSupported === false) {
104
- return Promise.reject(lifecycleUnsupportedError());
105
- }
106
- if (this.lifecycleSupported === null) {
107
- return new Promise((resolve, reject) => {
108
- pending.capabilityWaiters.push({ prompt, options, commandUuid, resolve, reject });
109
- });
110
- }
111
- return this.writeSteer(pending, prompt, options, commandUuid);
70
+ acceptRequest(request) {
71
+ request.admit?.({ status: 'submitted', submission: request.submission });
72
+ request.admit = null;
112
73
  }
113
- writeSteer(pending, prompt, options, commandUuid = randomUUID()) {
114
- if (this.pending !== pending) {
115
- return Promise.reject(capabilityUndecidedTurnEndedError());
116
- }
117
- if (!this.stdin.writable) {
118
- return Promise.reject(preAdmissionError('claude resident child is not running'));
119
- }
120
- // The steer joins this resident execution window: from here on it cannot drain
121
- // until this command has also reached a terminal lifecycle state.
122
- pending.submitted.push(commandUuid);
123
- return new Promise((resolve, reject) => {
124
- pending.writeWaiters.set(commandUuid, { resolve, reject });
125
- const fail = (error) => {
126
- // A steer whose write failed will never reach the CLI, so no
127
- // `command_lifecycle` is coming for it: mark it terminal here or the
128
- // turn waits on a signal that cannot arrive. Reject the waiter first,
129
- // since marking may settle the turn and settlement rejects surviving
130
- // waiters with the generic write-unconfirmed message.
131
- this.rejectWriteWaiter(pending, commandUuid, ambiguousWriteError(error));
132
- this.markCommandTerminal(pending, commandUuid, 'steer write failed');
133
- };
134
- try {
135
- this.stdin.write(`${buildUserMessage(prompt, options, commandUuid)}\n`, (err) => {
136
- if (err != null) {
137
- fail(err);
138
- return;
139
- }
140
- this.resolveWriteWaiter(pending, commandUuid);
141
- });
142
- }
143
- catch (error) {
144
- fail(error);
145
- }
146
- });
74
+ failWrite(uuid, request, error) {
75
+ if (this.requests.get(uuid) !== request || request.admit === null)
76
+ return;
77
+ this.requests.delete(uuid);
78
+ this.consumed.delete(uuid);
79
+ request.admit({ status: 'ambiguous', error });
80
+ request.admit = null;
81
+ request.settle({ kind: 'failed', error });
82
+ this.clearIdleIfEmpty();
147
83
  }
148
84
  onStdoutChunk(chunk) {
149
- for (const line of this.lineBuf.push(chunk))
85
+ if (this.closed)
86
+ return;
87
+ for (const line of this.lineBuf.push(chunk)) {
88
+ if (this.closed)
89
+ break;
150
90
  this.onLine(parseLine(line));
91
+ }
92
+ }
93
+ /** Actual transport loss fails each outstanding request, without a completion. */
94
+ fail(error) {
95
+ this.close({ kind: 'failed', error });
151
96
  }
152
- failPending(err) {
153
- this.settlePending(err)?.reject(err);
97
+ /** Explicit teardown stops requests and converges unconfirmed admissions. */
98
+ stop() {
99
+ this.close({ kind: 'stopped' });
100
+ }
101
+ close(settlement) {
102
+ this.closed = true;
103
+ const error = settlement.kind === 'failed'
104
+ ? settlement.error
105
+ : new Error('claude resident session stopped before write acknowledgement');
106
+ for (const request of this.requests.values()) {
107
+ request.admit?.(request.write === null
108
+ ? { status: 'ambiguous', error }
109
+ : settlement.kind === 'stopped' ? { status: 'stopped' } : { status: 'failed', error });
110
+ request.admit = null;
111
+ request.settle(settlement);
112
+ }
113
+ this.requests.clear();
114
+ this.consumed.clear();
115
+ this.aggregator.discard();
116
+ this.clearIdleIfEmpty();
154
117
  }
155
118
  enableRemoteControl() {
156
- if (!this.stdin.writable)
119
+ if (this.closed || !this.stdin.writable)
157
120
  return;
158
121
  this.remoteControlRequestId = randomUUID();
159
122
  this.stdin.write(`${buildRemoteControlEnable(this.remoteControlRequestId)}\n`);
160
123
  }
161
- /**
162
- * Detach the in-flight turn: clear its deadline timer and null `pending`,
163
- * returning it so the caller can resolve or reject it exactly once.
164
- */
165
- settlePending(failure) {
166
- const pending = this.pending;
167
- if (pending === null)
168
- return null;
169
- if (pending.timer !== null)
170
- clearTimeout(pending.timer);
171
- this.pending = null;
172
- // On an explicit failure (write error, stop, idle reap) both waiter kinds
173
- // get the real error. On a clean `result` settlement there is no error:
174
- // capability waiters never got a decision, while write waiters were written
175
- // but unconfirmed — distinct messages for distinct conditions.
176
- this.rejectCapabilityWaiters(pending, failure ?? capabilityUndecidedTurnEndedError());
177
- this.rejectWriteWaiters(pending, failure ?? steerWriteUnconfirmedError());
178
- return pending;
179
- }
180
- /**
181
- * Mark a submitted command as producing nothing further, then re-check
182
- * settlement. `abnormalReason` is `null` for the normal ending (`completed`)
183
- * and a short phrase for a command that never ran — those are logged,
184
- * because the CLI gives no other trace of a command it declined.
185
- *
186
- * A command ending abnormally never fails the turn by itself: the probe
187
- * shows a `cancelled` command coexisting with another that answers normally
188
- * (that is exactly what an interrupt looks like), so
189
- * rejecting on `cancelled` — as the pre-#342 code did — is wrong.
190
- */
191
- markCommandTerminal(pending, commandUuid, abnormalReason) {
192
- if (!pending.submitted.includes(commandUuid))
193
- return;
194
- if (!pending.terminal.has(commandUuid)) {
195
- pending.terminal.add(commandUuid);
196
- if (abnormalReason === null) {
197
- pending.ranAnyCommand = true;
198
- }
199
- else {
200
- pending.lastAbnormalReason = abnormalReason;
201
- this.options.log?.('warn', `claude command ${commandUuid} ${abnormalReason}; it will not ` +
202
- 'produce further output for this turn');
203
- }
124
+ clearIdleIfEmpty() {
125
+ if (this.requests.size === 0 && this.timer !== null) {
126
+ clearTimeout(this.timer);
127
+ this.timer = null;
204
128
  }
205
- this.settleIfReady(pending);
206
129
  }
207
- /**
208
- * The drainage gate: every submitted command has reached a terminal
209
- * lifecycle state AND at least one valid `result` has been seen. Result
210
- * identity and settlement have already been forwarded one-by-one.
211
- *
212
- * Two escapes, both anti-hang:
213
- *
214
- * - no lifecycle signal at all (`msg_lifecycle_v1` absent, so no
215
- * `command_lifecycle` will ever arrive) — the `result` is then the only
216
- * terminal event there is, so settle on it;
217
- * - every command terminal, none of them ever ran, and no result — nothing
218
- * can answer this turn, so fail it loudly. The idle deadline is not an
219
- * acceptable backstop here: it reaps the resident child, and any inbound
220
- * line re-arms it, so a healthy session could be killed long after the
221
- * turn became unanswerable.
222
- *
223
- * When a command *did* run but no result has arrived yet, this waits: the
224
- * probe shows terminal lifecycle states arriving both before and after the
225
- * result they belong to, so "terminal, therefore no result is coming" is not
226
- * a sound inference.
227
- */
228
- settleIfReady(pending) {
229
- if (this.pending !== pending)
230
- return;
231
- if (pending.sawResult && this.lifecycleSupported !== true) {
232
- this.settlePending()?.resolve();
233
- return;
234
- }
235
- if (pending.startedSinceResult.size > 0)
236
- return;
237
- for (const commandUuid of pending.submitted) {
238
- if (!pending.terminal.has(commandUuid))
239
- return;
240
- }
241
- if (pending.sawResult) {
242
- this.settlePending()?.resolve();
130
+ /** Outstanding work has a max-idle deadline; pure background work has none. */
131
+ armIdleTimer() {
132
+ if (this.requests.size === 0)
243
133
  return;
244
- }
245
- if (pending.ranAnyCommand)
246
- return;
247
- const error = new Error('claude turn ended without running any of its commands ' +
248
- `(last: ${pending.lastAbnormalReason ?? 'no command reached the CLI'})`);
249
- // Pass the cause into `settlePending` so surviving steer waiters get the
250
- // real reason instead of the generic write-unconfirmed message.
251
- this.settlePending(error)?.reject(error);
252
- }
253
- /**
254
- * (Re)arm the per-turn idle deadline. `turnTimeoutMs` is a *max-idle* window,
255
- * not a total-turn cap: any inbound stream line for this turn pushes it out
256
- * (see `onLine`). A genuinely wedged child (no stream activity for the whole
257
- * window) is still reaped — preserving the #120 anti-hang intent — but a long
258
- * but continuously-streaming turn never trips the deadline (#156).
259
- */
260
- armIdleTimer(pending) {
261
- if (pending.timer !== null)
262
- clearTimeout(pending.timer);
263
- pending.timer = setTimeout(() => {
264
- if (this.pending !== pending)
265
- return;
266
- const error = new Error(`claude resident turn stalled: no stream activity for ${this.options.turnTimeoutMs}ms`);
267
- const stalled = this.settlePending(error);
268
- if (stalled === null)
269
- return;
270
- this.options.log?.('error', `claude turn stalled: no stream activity for ${this.options.turnTimeoutMs}ms; reaping resident child`);
271
- stalled.reject(error);
272
- this.options.reapOnTimeout();
134
+ if (this.timer !== null)
135
+ clearTimeout(this.timer);
136
+ this.timer = setTimeout(() => {
137
+ const error = new Error(`claude resident requests stalled: no stream activity for ${this.options.turnTimeoutMs}ms`);
138
+ this.options.log?.('error', `${error.message}; reaping resident child`);
139
+ this.fail(error);
140
+ this.options.reapOnTimeout(error);
273
141
  }, this.options.turnTimeoutMs);
274
142
  }
275
143
  onLine(line) {
276
- // Idle-timeout reset: any inbound stream line for the pending turn is
277
- // activity, so push the deadline out. The terminal `result` clears the
278
- // timer via `settlePending` below.
279
- if (this.pending !== null)
280
- this.armIdleTimer(this.pending);
144
+ this.armIdleTimer();
281
145
  switch (line.kind) {
282
146
  case 'init':
147
+ this.aggregator.accept(line);
283
148
  this.decideLifecycleSupport(line.capabilities.includes('msg_lifecycle_v1'));
284
- this.pending?.aggregator.accept(line);
285
149
  break;
286
150
  case 'assistant':
287
- this.pending?.aggregator.accept(line);
151
+ this.aggregator.accept(line);
288
152
  this.options.onProtocolEvent?.({ kind: 'stream', line });
289
153
  break;
290
154
  case 'user':
@@ -292,74 +156,65 @@ export class ClaudeCodeStreamRpc {
292
156
  this.options.onProtocolEvent?.({ kind: 'stream', line });
293
157
  break;
294
158
  case 'command_lifecycle': {
295
- const pending = this.pending;
296
- if (pending === null || line.commandUuid === null)
297
- break;
298
- if (line.state !== null) {
299
- this.options.onProtocolEvent?.({
300
- kind: 'command_lifecycle',
301
- commandUuid: line.commandUuid,
302
- state: line.state,
303
- });
304
- }
305
- if (line.state === 'started')
306
- pending.startedSinceResult.add(line.commandUuid);
307
- // `command_lifecycle` does double duty: it coordinates live-steer
308
- // admission (writeWaiters) and proves lifecycle capability, and its
309
- // terminal states are the drainage gate when the CLI represents several
310
- // started commands with one `result`.
311
- const newlySupported = this.lifecycleSupported === null;
312
- if (newlySupported)
313
- this.lifecycleSupported = true;
314
- this.resolveWriteWaiter(pending, line.commandUuid);
315
- if (newlySupported && this.pending === pending) {
316
- this.flushCapabilityWaiters(pending);
317
- }
318
- if (this.pending !== pending)
159
+ const { commandUuid, state } = line;
160
+ if (commandUuid === null || state === null)
319
161
  break;
320
- if (line.state === 'completed') {
321
- this.markCommandTerminal(pending, line.commandUuid, null);
322
- }
323
- else if (line.state === 'cancelled' || line.state === 'discarded' || line.state === 'refused') {
324
- pending.startedSinceResult.delete(line.commandUuid);
325
- this.markCommandTerminal(pending, line.commandUuid, `was ${line.state} by claude`);
162
+ if (state === 'started')
163
+ this.consumed.add(commandUuid);
164
+ const request = this.requests.get(commandUuid);
165
+ if (request?.write === null) {
166
+ this.acceptRequest(request);
167
+ // Consumed commands can report cancelled before their failure result.
168
+ // Keep the result's members; lifecycle alone cannot supply its outcome.
169
+ if (state === 'discarded' || state === 'refused' ||
170
+ (state === 'cancelled' && !this.consumed.has(commandUuid))) {
171
+ this.requests.delete(commandUuid);
172
+ request.settle({ kind: 'failed', error: new Error(`claude command was ${state}`) });
173
+ this.clearIdleIfEmpty();
174
+ }
326
175
  }
176
+ // An unconsumed command's cancellation cannot discard generating text.
177
+ if (state === 'cancelled' && this.consumed.has(commandUuid))
178
+ this.aggregator.discard();
179
+ this.options.onProtocolEvent?.({ kind: 'command_lifecycle', commandUuid, state });
180
+ this.decideLifecycleSupport(true);
327
181
  break;
328
182
  }
329
183
  case 'result': {
330
- const pending = this.pending;
331
- if (pending === null) {
332
- const error = new Error('claude result envelope arrived without an attributable command group');
333
- this.options.log?.('error', error.message, error);
334
- this.options.reapOnTimeout();
335
- break;
184
+ this.aggregator.accept(line);
185
+ const outcome = this.aggregator.takeOutcome();
186
+ const commandUuids = new Set(this.consumed);
187
+ this.consumed.clear();
188
+ const uuid = line.outcome.userMessageUuid;
189
+ if (uuid !== null)
190
+ commandUuids.add(uuid);
191
+ if (this.lifecycleSupported !== true && uuid === null) {
192
+ for (const [id, request] of this.requests) {
193
+ if (request.write === null)
194
+ commandUuids.add(id);
195
+ }
336
196
  }
337
- const commandUuid = line.outcome.userMessageUuid;
338
- if (commandUuid !== null && !pending.submitted.includes(commandUuid)) {
339
- const error = new Error(`claude result envelope for unsubmitted command ${commandUuid}; ` +
340
- 'native completion ownership is ambiguous');
341
- this.options.log?.('error', error.message, error);
342
- this.settlePending(error)?.reject(error);
343
- this.options.reapOnTimeout();
344
- break;
197
+ const answered = [];
198
+ const submittedUuids = [];
199
+ for (const id of commandUuids) {
200
+ const request = this.requests.get(id);
201
+ if (request === undefined || request.write !== null)
202
+ continue;
203
+ this.requests.delete(id);
204
+ answered.push(request);
205
+ submittedUuids.push(id);
345
206
  }
346
- // The interrupt artifact (`error_during_execution` with no result) is
347
- // not a native answer boundary. Older valid results may omit the uuid.
348
- if (line.outcome.subtype === 'error_during_execution' &&
349
- line.outcome.userMessageUuid === null &&
350
- line.outcome.text === null) {
351
- this.options.log?.('warn', 'claude interrupt result artifact ignored');
352
- break;
207
+ this.clearIdleIfEmpty();
208
+ const completion = answered.length === 0 ? null : completionFromTurnOutcome(outcome, this.options.sessionId, this.options.outputSchemaEnabled === true);
209
+ // Remove the answered requests before callbacks can admit or stop work.
210
+ // Native end is still delivered before these submissions settle.
211
+ this.options.onProtocolEvent?.({ kind: 'result', outcome, commandUuids: submittedUuids });
212
+ for (const request of answered) {
213
+ this.acceptRequest(request);
214
+ request.settle({ kind: 'completion', completion: completion });
353
215
  }
354
- pending.aggregator.accept(line);
355
- const outcome = pending.aggregator.takeOutcome();
356
- pending.startedSinceResult.clear();
357
- pending.sawResult = true;
358
- this.options.onProtocolEvent?.({
359
- kind: 'result',
360
- outcome,
361
- });
362
- this.settleIfReady(pending);
216
+ if (this.lifecycleSupported !== true)
217
+ this.rejectWaitingRequests();
363
218
  break;
364
219
  }
365
220
  case 'control_request':
@@ -372,63 +227,34 @@ export class ClaudeCodeStreamRpc {
372
227
  this.options.log?.('warn', `claude stream-json parse error: ${line.raw}`);
373
228
  break;
374
229
  default:
375
- // Every other envelope (`system` notices past `init` and
376
- // `compact_boundary`, `stream_event`,
377
- // `rate_limit_event`, ...) carries nothing the runtime consumes. It
378
- // already counted as activity for the idle deadline above.
379
230
  break;
380
231
  }
381
232
  }
382
233
  decideLifecycleSupport(supported) {
383
- if (this.lifecycleSupported !== null)
234
+ if (this.lifecycleSupported === true)
384
235
  return;
385
- this.lifecycleSupported = supported;
386
- const pending = this.pending;
387
- if (pending === null)
236
+ if (!supported && this.lifecycleSupported !== null)
388
237
  return;
389
- if (supported) {
390
- this.flushCapabilityWaiters(pending);
238
+ this.lifecycleSupported = supported;
239
+ if (!supported) {
240
+ this.rejectWaitingRequests();
391
241
  return;
392
242
  }
393
- this.rejectCapabilityWaiters(pending, lifecycleUnsupportedError());
243
+ for (const request of this.requests.values())
244
+ request.write?.();
394
245
  }
395
- flushCapabilityWaiters(pending) {
396
- const waiters = pending.capabilityWaiters.splice(0);
397
- for (const waiter of waiters) {
398
- void this.writeSteer(pending, waiter.prompt, waiter.options, waiter.commandUuid).then(waiter.resolve, waiter.reject);
246
+ rejectWaitingRequests() {
247
+ const error = this.lifecycleSupported === false ? lifecycleUnsupportedError()
248
+ : new Error('claude result arrived before concurrent-input capability was decided');
249
+ for (const [uuid, request] of this.requests) {
250
+ if (request.write === null)
251
+ continue;
252
+ this.requests.delete(uuid);
253
+ request.admit?.({ status: 'failed', error });
254
+ request.admit = null;
255
+ request.settle({ kind: 'failed', error });
399
256
  }
400
- }
401
- rejectCapabilityWaiters(pending, error) {
402
- const waiters = pending.capabilityWaiters.splice(0);
403
- const failure = error instanceof ClaudeSteerAdmissionError
404
- ? error
405
- : preAdmissionError(error.message, error);
406
- for (const waiter of waiters)
407
- waiter.reject(failure);
408
- }
409
- resolveWriteWaiter(pending, commandUuid) {
410
- const waiter = pending.writeWaiters.get(commandUuid);
411
- if (waiter === undefined)
412
- return;
413
- pending.writeWaiters.delete(commandUuid);
414
- waiter.resolve();
415
- }
416
- rejectWriteWaiter(pending, commandUuid, error) {
417
- const waiter = pending.writeWaiters.get(commandUuid);
418
- if (waiter === undefined)
419
- return;
420
- pending.writeWaiters.delete(commandUuid);
421
- waiter.reject(error);
422
- }
423
- rejectWriteWaiters(pending, error) {
424
- const failure = error instanceof ClaudeSteerAdmissionError &&
425
- error.admission === 'ambiguous'
426
- ? error
427
- : ambiguousWriteError(error);
428
- const waiters = [...pending.writeWaiters.values()];
429
- pending.writeWaiters.clear();
430
- for (const waiter of waiters)
431
- waiter.reject(failure);
257
+ this.clearIdleIfEmpty();
432
258
  }
433
259
  onControlRequest(requestId, subtype, request) {
434
260
  if (requestId === null || !this.stdin.writable)
@@ -468,21 +294,7 @@ export class ClaudeCodeStreamRpc {
468
294
  }
469
295
  }
470
296
  function lifecycleUnsupportedError() {
471
- return preAdmissionError('claude resident session cannot prove live-steer lifecycle: ' +
472
- 'msg_lifecycle_v1 is unavailable');
473
- }
474
- function capabilityUndecidedTurnEndedError() {
475
- return preAdmissionError('claude resident turn ended before live-steer capability was decided');
476
- }
477
- function steerWriteUnconfirmedError() {
478
- return new ClaudeSteerAdmissionError('ambiguous', 'claude resident turn ended before the steer write was confirmed');
479
- }
480
- function preAdmissionError(message, cause) {
481
- return new ClaudeSteerAdmissionError('failed', message, cause === undefined ? undefined : { cause });
482
- }
483
- function ambiguousWriteError(error) {
484
- const cause = asError(error);
485
- return new ClaudeSteerAdmissionError('ambiguous', cause.message, { cause });
297
+ return new Error('claude resident session cannot attribute concurrent inputs: msg_lifecycle_v1 is unavailable');
486
298
  }
487
299
  function asError(error) {
488
300
  return error instanceof Error ? error : new Error(String(error));