@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/README.md +92 -44
- package/dist/config.d.ts +2 -2
- package/dist/rpc.d.ts +27 -93
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +181 -369
- package/dist/rpc.js.map +1 -1
- package/dist/runtime-activity.d.ts +33 -0
- package/dist/runtime-activity.d.ts.map +1 -0
- package/dist/{runtime-submissions.js → runtime-activity.js} +25 -118
- package/dist/runtime-activity.js.map +1 -0
- package/dist/runtime-session.d.ts +6 -2
- package/dist/runtime-session.d.ts.map +1 -1
- package/dist/runtime-session.js +20 -11
- package/dist/runtime-session.js.map +1 -1
- package/dist/runtime.d.ts +3 -14
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +37 -152
- package/dist/runtime.js.map +1 -1
- package/dist/stream.d.ts +7 -3
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +20 -11
- package/dist/stream.js.map +1 -1
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +24 -33
- package/dist/supervisor.js.map +1 -1
- package/dist/types.d.ts +26 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
- package/dist/admission-classify.d.ts +0 -7
- package/dist/admission-classify.d.ts.map +0 -1
- package/dist/admission-classify.js +0 -17
- package/dist/admission-classify.js.map +0 -1
- package/dist/runtime-submissions.d.ts +0 -58
- package/dist/runtime-submissions.d.ts.map +0 -1
- package/dist/runtime-submissions.js.map +0 -1
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
if (!this.stdin.writable) {
|
|
56
|
-
return Promise.
|
|
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
|
-
|
|
59
|
-
|
|
29
|
+
const concurrent = this.requests.size > 0;
|
|
30
|
+
if (concurrent && this.lifecycleSupported === false) {
|
|
31
|
+
return Promise.resolve({ status: 'failed', error: lifecycleUnsupportedError() });
|
|
60
32
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
114
|
-
if (this.
|
|
115
|
-
return
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
296
|
-
if (
|
|
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 (
|
|
321
|
-
this.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
this.
|
|
344
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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
|
|
234
|
+
if (this.lifecycleSupported === true)
|
|
384
235
|
return;
|
|
385
|
-
this.lifecycleSupported
|
|
386
|
-
const pending = this.pending;
|
|
387
|
-
if (pending === null)
|
|
236
|
+
if (!supported && this.lifecycleSupported !== null)
|
|
388
237
|
return;
|
|
389
|
-
|
|
390
|
-
|
|
238
|
+
this.lifecycleSupported = supported;
|
|
239
|
+
if (!supported) {
|
|
240
|
+
this.rejectWaitingRequests();
|
|
391
241
|
return;
|
|
392
242
|
}
|
|
393
|
-
this.
|
|
243
|
+
for (const request of this.requests.values())
|
|
244
|
+
request.write?.();
|
|
394
245
|
}
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
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
|
|
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));
|