@parall/claude-agent 1.58.2 → 1.60.0
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/busy-state.d.ts +56 -0
- package/dist/busy-state.d.ts.map +1 -0
- package/dist/busy-state.js +140 -0
- package/dist/compact.d.ts +29 -0
- package/dist/compact.d.ts.map +1 -0
- package/dist/compact.js +98 -0
- package/dist/dispatch.d.ts +54 -6
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +193 -180
- package/dist/index.js +6 -2
- package/dist/input-lifecycle.d.ts +31 -1
- package/dist/input-lifecycle.d.ts.map +1 -1
- package/dist/input-lifecycle.js +50 -2
- package/dist/output-parser.d.ts +32 -0
- package/dist/output-parser.d.ts.map +1 -1
- package/dist/output-parser.js +106 -10
- package/dist/process-pump.d.ts +105 -0
- package/dist/process-pump.d.ts.map +1 -0
- package/dist/process-pump.js +436 -0
- package/dist/runtime-turn.d.ts +19 -0
- package/dist/runtime-turn.d.ts.map +1 -0
- package/dist/runtime-turn.js +59 -0
- package/dist/session-manager.d.ts +1 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +3 -0
- package/dist/turn-sink.d.ts +49 -0
- package/dist/turn-sink.d.ts.map +1 -0
- package/dist/turn-sink.js +9 -0
- package/package.json +4 -4
- package/src/busy-state.ts +159 -0
- package/src/compact.ts +132 -0
- package/src/dispatch.ts +217 -209
- package/src/index.ts +6 -1
- package/src/input-lifecycle.ts +61 -1
- package/src/output-parser.ts +140 -10
- package/src/process-pump.ts +493 -0
- package/src/runtime-turn.ts +80 -0
- package/src/session-manager.ts +4 -0
- package/src/turn-sink.ts +59 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { describeRuntimeTurnTrigger, } from '@parall/agent-core';
|
|
2
|
+
import { ClaudeBusyTracker } from './busy-state.js';
|
|
3
|
+
import { ClaudeRuntimeTurn } from './runtime-turn.js';
|
|
4
|
+
import { classifyClaudeTurn } from './turn-outcome.js';
|
|
5
|
+
/**
|
|
6
|
+
* Single owner of one Claude subprocess's stdout. Reads every frame the CLI
|
|
7
|
+
* emits — during dispatches AND between them — and routes it:
|
|
8
|
+
*
|
|
9
|
+
* - `command_lifecycle` frames go to the delivery they name (stdin uuid).
|
|
10
|
+
* - Content frames (thinking / text / tool_call / tool_result / error) go to
|
|
11
|
+
* the delivery whose lifecycle is `started` and not yet terminal
|
|
12
|
+
* (claude-soft-steer-design.md: the lifecycle terminal, not `result`,
|
|
13
|
+
* bounds a delivery); with no started delivery they go to the open
|
|
14
|
+
* runtime-initiated turn, opening one if needed. A `queued` delivery never
|
|
15
|
+
* owns frames.
|
|
16
|
+
* - `result` frames attach turn-outcome evidence to the owner and close a
|
|
17
|
+
* runtime-initiated turn; a poison `num_turns: 0` resume artifact never
|
|
18
|
+
* opens or closes one.
|
|
19
|
+
* - task frames feed the busy ledger; `runtime_activity` only refreshes
|
|
20
|
+
* inactivity deadlines.
|
|
21
|
+
*
|
|
22
|
+
* Reading continuously also removes the latent stall where an unread
|
|
23
|
+
* self-started turn filled the OS pipe and blocked the CLI.
|
|
24
|
+
*/
|
|
25
|
+
export class ClaudeProcessPump {
|
|
26
|
+
opts;
|
|
27
|
+
busy;
|
|
28
|
+
capabilities = new Set(['msg_lifecycle_v1']);
|
|
29
|
+
sessionId;
|
|
30
|
+
runtimeTurn = null;
|
|
31
|
+
closed = false;
|
|
32
|
+
idleWaiters = [];
|
|
33
|
+
idleTimer = null;
|
|
34
|
+
activeDrain = null;
|
|
35
|
+
now;
|
|
36
|
+
constructor(opts) {
|
|
37
|
+
this.opts = opts;
|
|
38
|
+
this.now = opts.now ?? Date.now;
|
|
39
|
+
this.busy = new ClaudeBusyTracker(opts.followUpHoldMs);
|
|
40
|
+
}
|
|
41
|
+
start() {
|
|
42
|
+
void this.run();
|
|
43
|
+
}
|
|
44
|
+
/** The dispatch generator currently draining `delivery` (owner preference). */
|
|
45
|
+
setActiveDrain(delivery, noteActivity) {
|
|
46
|
+
this.activeDrain = delivery;
|
|
47
|
+
if (noteActivity)
|
|
48
|
+
delivery.noteActivity = noteActivity;
|
|
49
|
+
}
|
|
50
|
+
clearActiveDrain(delivery) {
|
|
51
|
+
if (this.activeDrain === delivery)
|
|
52
|
+
this.activeDrain = null;
|
|
53
|
+
}
|
|
54
|
+
hasStartedDelivery() {
|
|
55
|
+
for (const delivery of this.opts.inputs.values())
|
|
56
|
+
if (isLive(delivery))
|
|
57
|
+
return true;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The CLI has (or is about to start) work no dispatch asked for: a
|
|
62
|
+
* runtime-initiated turn is open, or a finished background task's
|
|
63
|
+
* follow-up hold is pending. Lazy restarts, dispatch aborts and
|
|
64
|
+
* `whenIdle` all defer to it.
|
|
65
|
+
*/
|
|
66
|
+
hasOwnWork(now = this.now()) {
|
|
67
|
+
return this.runtimeTurn !== null || this.busy.holdActive(now);
|
|
68
|
+
}
|
|
69
|
+
busyState(now = this.now()) {
|
|
70
|
+
const outstanding = this.busy.outstanding();
|
|
71
|
+
const holdUntil = this.busy.activeHoldUntil(now);
|
|
72
|
+
return {
|
|
73
|
+
activeTurns: this.hasStartedDelivery() || this.runtimeTurn ? 1 : 0,
|
|
74
|
+
backgroundWork: outstanding.total - outstanding.ambient,
|
|
75
|
+
...(holdUntil !== undefined ? { holdUntil } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Dispatch abort: fail every live delivery and release its drain. */
|
|
79
|
+
abortDeliveries(message) {
|
|
80
|
+
for (const delivery of this.opts.inputs.values()) {
|
|
81
|
+
if (delivery.terminal)
|
|
82
|
+
continue;
|
|
83
|
+
void this.opts.inputs.failBestEffort(delivery, this.opts.log);
|
|
84
|
+
delivery.sink.push({ kind: 'ended', reason: 'aborted', message });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The adapter is terminating the process (kill / reset / shutdown): every
|
|
89
|
+
* drain and the open runtime turn end now, before stdout EOF is observed.
|
|
90
|
+
*/
|
|
91
|
+
close(reason, message) {
|
|
92
|
+
if (this.closed)
|
|
93
|
+
return;
|
|
94
|
+
this.closed = true;
|
|
95
|
+
for (const delivery of this.opts.inputs.values()) {
|
|
96
|
+
delivery.sink.push({ kind: 'ended', reason, message });
|
|
97
|
+
delivery.sink.close();
|
|
98
|
+
}
|
|
99
|
+
this.closeRuntimeTurn(reason, message);
|
|
100
|
+
this.busy.reset();
|
|
101
|
+
this.checkIdle();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Run `cb` once the CLI has nothing left to do on its own: no
|
|
105
|
+
* runtime-initiated turn open and no follow-up hold pending (a hold that
|
|
106
|
+
* turns into a follow-up turn is waited for as well). Immediate when
|
|
107
|
+
* already idle or closed.
|
|
108
|
+
*/
|
|
109
|
+
whenIdle(cb) {
|
|
110
|
+
this.idleWaiters.push(cb);
|
|
111
|
+
this.checkIdle();
|
|
112
|
+
}
|
|
113
|
+
checkIdle() {
|
|
114
|
+
if (this.idleWaiters.length === 0)
|
|
115
|
+
return;
|
|
116
|
+
if (this.idleTimer) {
|
|
117
|
+
clearTimeout(this.idleTimer);
|
|
118
|
+
this.idleTimer = null;
|
|
119
|
+
}
|
|
120
|
+
if (!this.closed) {
|
|
121
|
+
if (this.runtimeTurn)
|
|
122
|
+
return; // closeRuntimeTurn re-checks
|
|
123
|
+
const now = this.now();
|
|
124
|
+
const holdUntil = this.busy.activeHoldUntil(now);
|
|
125
|
+
if (holdUntil !== undefined) {
|
|
126
|
+
this.idleTimer = setTimeout(() => {
|
|
127
|
+
this.idleTimer = null;
|
|
128
|
+
this.checkIdle();
|
|
129
|
+
}, holdUntil - now + 1);
|
|
130
|
+
this.idleTimer.unref?.();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const waiters = this.idleWaiters.splice(0);
|
|
135
|
+
for (const waiter of waiters)
|
|
136
|
+
waiter();
|
|
137
|
+
}
|
|
138
|
+
closeRuntimeTurn(reason, message) {
|
|
139
|
+
const turn = this.runtimeTurn;
|
|
140
|
+
if (!turn)
|
|
141
|
+
return;
|
|
142
|
+
this.runtimeTurn = null;
|
|
143
|
+
turn.sink.push({ kind: 'ended', reason, message });
|
|
144
|
+
turn.sink.close();
|
|
145
|
+
this.opts.log?.info?.(`runtime-initiated turn ${turn.groupKey} on ${this.opts.sessionKey} closed (${reason})`);
|
|
146
|
+
this.opts.hooks.onRuntimeTurnClosed();
|
|
147
|
+
this.checkIdle();
|
|
148
|
+
}
|
|
149
|
+
// --- routing --------------------------------------------------------------
|
|
150
|
+
async run() {
|
|
151
|
+
const { parser, log } = this.opts;
|
|
152
|
+
try {
|
|
153
|
+
for await (const parsed of parser) {
|
|
154
|
+
if (this.closed)
|
|
155
|
+
break;
|
|
156
|
+
this.touchAll();
|
|
157
|
+
await this.route(parsed);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
log?.warn?.(`Claude stdout pump failed for ${this.opts.sessionKey}: ${String(err)}`);
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
await this.handleEof();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async route(parsed) {
|
|
168
|
+
switch (parsed.type) {
|
|
169
|
+
case 'runtime_activity':
|
|
170
|
+
return;
|
|
171
|
+
case 'runtime_init':
|
|
172
|
+
this.handleInit(parsed);
|
|
173
|
+
return;
|
|
174
|
+
case 'command_lifecycle':
|
|
175
|
+
await this.handleLifecycle(parsed);
|
|
176
|
+
return;
|
|
177
|
+
case 'runtime_task':
|
|
178
|
+
this.handleTask(parsed);
|
|
179
|
+
return;
|
|
180
|
+
case 'user_text':
|
|
181
|
+
// The CLI's own injected notice (task-notification text); the
|
|
182
|
+
// follow-up turn's content frames carry the consequence.
|
|
183
|
+
return;
|
|
184
|
+
case 'turn_end':
|
|
185
|
+
await this.handleTurnEnd(parsed);
|
|
186
|
+
return;
|
|
187
|
+
case 'assistant_error': {
|
|
188
|
+
const owner = this.ownerFor(true);
|
|
189
|
+
owner?.evidence.noticeTexts.push(parsed.message);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
case 'compact_boundary': {
|
|
193
|
+
// The CLI folded history: a `/compact` command's evidence, or its
|
|
194
|
+
// own auto-compact mid-turn. Progress only, never a RuntimeEvent.
|
|
195
|
+
const owner = this.ownerFor(false);
|
|
196
|
+
if (owner) {
|
|
197
|
+
owner.evidence.compactBoundary = {
|
|
198
|
+
...(parsed.preTokens !== undefined ? { preTokens: parsed.preTokens } : {}),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
case 'runtime_session':
|
|
204
|
+
case 'turn_outcome':
|
|
205
|
+
// Never produced by the parser (type guards only).
|
|
206
|
+
return;
|
|
207
|
+
default: {
|
|
208
|
+
const owner = this.ownerFor(true);
|
|
209
|
+
if (!owner)
|
|
210
|
+
return;
|
|
211
|
+
if (parsed.type === 'error')
|
|
212
|
+
owner.evidence.sawError = true;
|
|
213
|
+
owner.sink.push({ kind: 'runtime', event: parsed });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
touchAll() {
|
|
219
|
+
for (const delivery of this.opts.inputs.values()) {
|
|
220
|
+
if (!delivery.terminal)
|
|
221
|
+
delivery.noteActivity?.();
|
|
222
|
+
}
|
|
223
|
+
this.runtimeTurn?.touch();
|
|
224
|
+
}
|
|
225
|
+
liveDeliveries() {
|
|
226
|
+
const live = [];
|
|
227
|
+
for (const delivery of this.opts.inputs.values())
|
|
228
|
+
if (isLive(delivery))
|
|
229
|
+
live.push(delivery);
|
|
230
|
+
return live;
|
|
231
|
+
}
|
|
232
|
+
ownerFor(open) {
|
|
233
|
+
if (this.activeDrain && isLive(this.activeDrain))
|
|
234
|
+
return this.activeDrain;
|
|
235
|
+
for (const delivery of this.opts.inputs.values())
|
|
236
|
+
if (isLive(delivery))
|
|
237
|
+
return delivery;
|
|
238
|
+
if (this.runtimeTurn)
|
|
239
|
+
return this.runtimeTurn;
|
|
240
|
+
return open ? this.openRuntimeTurn() : null;
|
|
241
|
+
}
|
|
242
|
+
openRuntimeTurn() {
|
|
243
|
+
const notification = this.busy.takeRecentNotification(this.now());
|
|
244
|
+
const trigger = notification
|
|
245
|
+
? {
|
|
246
|
+
kind: 'background_task',
|
|
247
|
+
...(notification.taskId ? { taskId: notification.taskId } : {}),
|
|
248
|
+
...(notification.description ? { description: notification.description } : {}),
|
|
249
|
+
...(notification.summary ? { summary: notification.summary } : {}),
|
|
250
|
+
...(notification.status ? { status: notification.status } : {}),
|
|
251
|
+
}
|
|
252
|
+
: { kind: 'runtime' };
|
|
253
|
+
const turn = new ClaudeRuntimeTurn(this.opts.sessionKey, trigger, {
|
|
254
|
+
onDetach: (reason) => this.closeRuntimeTurn('detached', reason),
|
|
255
|
+
log: this.opts.log,
|
|
256
|
+
});
|
|
257
|
+
this.runtimeTurn = turn;
|
|
258
|
+
this.busy.clearHold();
|
|
259
|
+
if (this.sessionId) {
|
|
260
|
+
turn.sink.push({
|
|
261
|
+
kind: 'runtime',
|
|
262
|
+
event: {
|
|
263
|
+
type: 'runtime_session',
|
|
264
|
+
runtimeSessionId: this.sessionId,
|
|
265
|
+
runtimeLaneKey: this.opts.sessionKey,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
this.opts.log?.info?.(`runtime-initiated turn ${turn.groupKey} opened on ${this.opts.sessionKey} (${describeRuntimeTurnTrigger(trigger)})`);
|
|
270
|
+
this.opts.hooks.onRuntimeTurnOpened(turn);
|
|
271
|
+
return turn;
|
|
272
|
+
}
|
|
273
|
+
handleInit(init) {
|
|
274
|
+
this.capabilities = new Set(init.capabilities);
|
|
275
|
+
if (init.sessionId)
|
|
276
|
+
this.sessionId = init.sessionId;
|
|
277
|
+
const fatal = this.opts.hooks.onRuntimeInit(init);
|
|
278
|
+
if (fatal) {
|
|
279
|
+
this.pushErrorToDeliveries(fatal);
|
|
280
|
+
this.opts.hooks.onFatal();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (!this.sessionId)
|
|
284
|
+
return;
|
|
285
|
+
const announce = {
|
|
286
|
+
type: 'runtime_session',
|
|
287
|
+
runtimeSessionId: this.sessionId,
|
|
288
|
+
runtimeLaneKey: this.opts.sessionKey,
|
|
289
|
+
};
|
|
290
|
+
for (const delivery of this.opts.inputs.values()) {
|
|
291
|
+
if (delivery.terminal || delivery.sessionAnnounced)
|
|
292
|
+
continue;
|
|
293
|
+
delivery.sessionAnnounced = true;
|
|
294
|
+
delivery.sink.push({ kind: 'runtime', event: announce });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async handleLifecycle(parsed) {
|
|
298
|
+
if (!this.capabilities.has('msg_lifecycle_v1')) {
|
|
299
|
+
this.pushErrorToDeliveries('Claude emitted command lifecycle before advertising msg_lifecycle_v1');
|
|
300
|
+
this.opts.hooks.onFatal();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const delivery = this.opts.inputs.getByCommand(parsed.commandUuid);
|
|
304
|
+
if (!delivery) {
|
|
305
|
+
this.opts.log?.warn?.(`ignoring lifecycle for unknown Claude command ${parsed.commandUuid}`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (parsed.state === 'started') {
|
|
309
|
+
this.busy.clearHold();
|
|
310
|
+
// A dispatch delivery took over stdout: the runtime-initiated turn's
|
|
311
|
+
// tail (if the CLI folded the delivery into it) belongs to the
|
|
312
|
+
// delivery from here on.
|
|
313
|
+
this.closeRuntimeTurn('absorbed');
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
await this.opts.inputs.apply(delivery, parsed.state);
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
await this.opts.inputs.failBestEffort(delivery, this.opts.log);
|
|
320
|
+
delivery.sink.push({
|
|
321
|
+
kind: 'runtime',
|
|
322
|
+
event: { type: 'error', message: `Claude input lifecycle update failed: ${String(err)}` },
|
|
323
|
+
});
|
|
324
|
+
this.opts.hooks.onFatal();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (parsed.state === 'completed' ||
|
|
328
|
+
parsed.state === 'cancelled' ||
|
|
329
|
+
parsed.state === 'discarded') {
|
|
330
|
+
delivery.sink.push({ kind: 'terminal' });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
handleTask(frame) {
|
|
334
|
+
const before = this.busy.outstanding().total;
|
|
335
|
+
this.busy.onTaskFrame(frame, this.now());
|
|
336
|
+
const log = this.opts.log;
|
|
337
|
+
switch (frame.subtype) {
|
|
338
|
+
case 'task_started':
|
|
339
|
+
log?.info?.(`background task ${frame.taskId ?? '?'} started on ${this.opts.sessionKey}: ${frame.description ?? frame.taskType ?? ''}`.trim());
|
|
340
|
+
return;
|
|
341
|
+
case 'task_notification':
|
|
342
|
+
log?.info?.(`background task ${frame.taskId ?? '?'} ${frame.status ?? 'finished'} on ${this.opts.sessionKey}; holding for its follow-up turn`);
|
|
343
|
+
return;
|
|
344
|
+
case 'background_tasks_changed': {
|
|
345
|
+
const after = this.busy.outstanding();
|
|
346
|
+
if (after.total !== before) {
|
|
347
|
+
log?.info?.(`background tasks on ${this.opts.sessionKey}: ${after.total} live (${after.ambient} ambient)`);
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
default:
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async handleTurnEnd(parsed) {
|
|
356
|
+
const live = this.liveDeliveries();
|
|
357
|
+
const owner = this.ownerFor(false);
|
|
358
|
+
// The poison startup frame (num_turns 0, --resume artifact) is not a
|
|
359
|
+
// turn boundary: it never overwrites evidence, opens or closes a turn.
|
|
360
|
+
if (parsed.numTurns !== 0 && owner)
|
|
361
|
+
owner.evidence.lastResultMeta = parsed.resultMeta;
|
|
362
|
+
if (parsed.numTurns === 0 && owner)
|
|
363
|
+
owner.evidence.zeroTurnResultMeta = parsed.resultMeta;
|
|
364
|
+
if (parsed.isError) {
|
|
365
|
+
const failed = parsed.userMessageUuid
|
|
366
|
+
? this.opts.inputs.getByCommand(parsed.userMessageUuid)
|
|
367
|
+
: undefined;
|
|
368
|
+
if (failed) {
|
|
369
|
+
failed.resultFailed = true;
|
|
370
|
+
if (settledAsLimit(failed.evidence))
|
|
371
|
+
failed.suppressFailReport = true;
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (live.length > 0) {
|
|
375
|
+
// An error result nobody claims while a delivery runs: fail every
|
|
376
|
+
// input and drop the process (the pre-pump contract).
|
|
377
|
+
if (owner && settledAsLimit(owner.evidence)) {
|
|
378
|
+
for (const delivery of this.opts.inputs.values())
|
|
379
|
+
delivery.suppressFailReport = true;
|
|
380
|
+
}
|
|
381
|
+
await this.opts.inputs.failAllBestEffort(this.opts.log);
|
|
382
|
+
for (const delivery of live) {
|
|
383
|
+
delivery.sink.push({ kind: 'ended', reason: 'error_result' });
|
|
384
|
+
}
|
|
385
|
+
this.opts.hooks.onFatal();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (owner && owner === this.runtimeTurn) {
|
|
389
|
+
this.closeRuntimeTurn('error_result');
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (parsed.numTurns !== 0 && owner && owner === this.runtimeTurn) {
|
|
394
|
+
this.closeRuntimeTurn('result');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
pushErrorToDeliveries(message) {
|
|
398
|
+
for (const delivery of this.opts.inputs.values()) {
|
|
399
|
+
if (delivery.terminal)
|
|
400
|
+
continue;
|
|
401
|
+
delivery.sink.push({ kind: 'runtime', event: { type: 'error', message } });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async handleEof() {
|
|
405
|
+
if (this.closed)
|
|
406
|
+
return;
|
|
407
|
+
this.closed = true;
|
|
408
|
+
const { handle, inputs, log } = this.opts;
|
|
409
|
+
const detail = handle.stderrChunks.join('').trim();
|
|
410
|
+
const exit = await handle.exitPromise.catch(() => ({ code: null, signal: null }));
|
|
411
|
+
if (detail)
|
|
412
|
+
log?.warn?.(`subprocess stderr: ${detail}`);
|
|
413
|
+
this.opts.hooks.onEof();
|
|
414
|
+
const message = detail ||
|
|
415
|
+
`Claude exited with code ${exit.code ?? 'unknown'}${exit.signal ? ` (${exit.signal})` : ''}`;
|
|
416
|
+
for (const delivery of inputs.values()) {
|
|
417
|
+
if (!delivery.terminal && settledAsLimit(delivery.evidence))
|
|
418
|
+
delivery.suppressFailReport = true;
|
|
419
|
+
}
|
|
420
|
+
await inputs.failAllBestEffort(log);
|
|
421
|
+
for (const delivery of inputs.values()) {
|
|
422
|
+
delivery.sink.push({ kind: 'ended', reason: 'eof', message });
|
|
423
|
+
delivery.sink.close();
|
|
424
|
+
}
|
|
425
|
+
this.closeRuntimeTurn('eof', message);
|
|
426
|
+
this.busy.reset();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/** usage_limit settles via the lane-level deferred complete, never a failed-input report. */
|
|
430
|
+
function settledAsLimit(evidence) {
|
|
431
|
+
return (classifyClaudeTurn(evidence.lastResultMeta, evidence.noticeTexts).outcome === 'usage_limit');
|
|
432
|
+
}
|
|
433
|
+
/** A delivery that owns stdout: the CLI reported `started` and no terminal state yet. */
|
|
434
|
+
function isLive(delivery) {
|
|
435
|
+
return delivery.reportedState === 'started' && !delivery.terminal;
|
|
436
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type GatewayLogger, type RuntimeEvent, RuntimeTurnBase, type RuntimeTurnTrigger } from '@parall/agent-core';
|
|
2
|
+
import { type ClaudeTurnSink, type TurnEvidence } from './turn-sink.js';
|
|
3
|
+
/**
|
|
4
|
+
* A turn the Claude CLI started on its own (no dispatch wrote to stdin): the
|
|
5
|
+
* follow-up it runs after a background task or background subagent finishes,
|
|
6
|
+
* or any other self-continuation. The pump routes its frames here; the
|
|
7
|
+
* gateway (main session) or the adapter's log-only fallback drains `events`.
|
|
8
|
+
*/
|
|
9
|
+
export declare class ClaudeRuntimeTurn extends RuntimeTurnBase {
|
|
10
|
+
readonly sink: ClaudeTurnSink;
|
|
11
|
+
readonly evidence: TurnEvidence;
|
|
12
|
+
constructor(sessionKey: string, trigger: RuntimeTurnTrigger, opts: {
|
|
13
|
+
onDetach: (reason: string) => void;
|
|
14
|
+
log?: GatewayLogger;
|
|
15
|
+
});
|
|
16
|
+
protected drain(): AsyncGenerator<RuntimeEvent>;
|
|
17
|
+
private finish;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=runtime-turn.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-turn.d.ts","sourceRoot":"","sources":["../src/runtime-turn.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAElB,KAAK,YAAY,EACjB,eAAe,EACf,KAAK,kBAAkB,EACxB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAEL,KAAK,cAAc,EAGnB,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AAExB;;;;;GAKG;AACH,qBAAa,iBAAkB,SAAQ,eAAe;IACpD,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAqB;gBAGlD,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE;QAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,GAAG,CAAC,EAAE,aAAa,CAAA;KAAE;cAMlD,KAAK,IAAI,cAAc,CAAC,YAAY,CAAC;IAetD,OAAO,CAAE,MAAM;CA6BhB"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { projectRuntimeEvent, RuntimeTurnBase, } from '@parall/agent-core';
|
|
2
|
+
import { classifyClaudeTurn } from './turn-outcome.js';
|
|
3
|
+
import { newTurnEvidence, newTurnSink, } from './turn-sink.js';
|
|
4
|
+
/**
|
|
5
|
+
* A turn the Claude CLI started on its own (no dispatch wrote to stdin): the
|
|
6
|
+
* follow-up it runs after a background task or background subagent finishes,
|
|
7
|
+
* or any other self-continuation. The pump routes its frames here; the
|
|
8
|
+
* gateway (main session) or the adapter's log-only fallback drains `events`.
|
|
9
|
+
*/
|
|
10
|
+
export class ClaudeRuntimeTurn extends RuntimeTurnBase {
|
|
11
|
+
sink;
|
|
12
|
+
evidence = newTurnEvidence();
|
|
13
|
+
constructor(sessionKey, trigger, opts) {
|
|
14
|
+
super(sessionKey, trigger, opts.onDetach);
|
|
15
|
+
this.sink = newTurnSink(`runtime-turn ${this.groupKey}`, opts.log);
|
|
16
|
+
}
|
|
17
|
+
async *drain() {
|
|
18
|
+
while (true) {
|
|
19
|
+
const next = await this.sink.next();
|
|
20
|
+
if (next.done)
|
|
21
|
+
return;
|
|
22
|
+
const envelope = next.value;
|
|
23
|
+
if (envelope.kind === 'runtime') {
|
|
24
|
+
yield projectRuntimeEvent(envelope.event, this.groupKey);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (envelope.kind === 'terminal')
|
|
28
|
+
continue;
|
|
29
|
+
yield* this.finish(envelope.reason, envelope.message);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
*finish(reason, message) {
|
|
34
|
+
switch (reason) {
|
|
35
|
+
case 'absorbed':
|
|
36
|
+
// A dispatch delivery took over the tail: its drain carries the
|
|
37
|
+
// result frame and the outcome; nothing to classify here.
|
|
38
|
+
return;
|
|
39
|
+
case 'result':
|
|
40
|
+
case 'error_result':
|
|
41
|
+
if (this.evidence.lastResultMeta) {
|
|
42
|
+
yield classifyClaudeTurn(this.evidence.lastResultMeta, this.evidence.noticeTexts);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
default:
|
|
46
|
+
if (!this.evidence.sawError) {
|
|
47
|
+
yield {
|
|
48
|
+
type: 'error',
|
|
49
|
+
message: message ?? `runtime-initiated turn ${reason}`,
|
|
50
|
+
groupKey: this.groupKey,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// No result frame classifies as runtime_crash (agent-turn-outcome
|
|
54
|
+
// §4.1); with evidence, classify what the CLI reported.
|
|
55
|
+
yield classifyClaudeTurn(this.evidence.lastResultMeta, this.evidence.noticeTexts);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -20,6 +20,7 @@ export declare class ClaudeSessionManager {
|
|
|
20
20
|
private readonly processes;
|
|
21
21
|
constructor(mainSessionKey: string, stateFilePath: string, logger?: ClaudeSessionManagerLogger | undefined);
|
|
22
22
|
getSessionId(sessionKey: string): string | undefined;
|
|
23
|
+
isMain(sessionKey: string): boolean;
|
|
23
24
|
getResumeArgs(sessionKey: string): string[];
|
|
24
25
|
recordSessionId(sessionKey: string, sessionId: string): void;
|
|
25
26
|
createForkSession(parentSessionKey: string): ForkSessionHandle | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAO5D,KAAK,0BAA0B,GAAG;IAChC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,8BAA8B,CAAC;IACrC,WAAW,EAAE,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAC7E,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAEF,qBAAa,oBAAoB;IAM7B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAP1B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IACxD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA6B;IAChE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;gBAGjD,cAAc,EAAE,MAAM,EACtB,aAAa,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,0BAA0B,YAAA;IAKtD,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIpD,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE;IAU3C,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAQrD,iBAAiB,CAAC,gBAAgB,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAYrE,WAAW,CAAC,UAAU,EAAE,MAAM;IAU9B,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAI/D,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB;IAsB/D,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,mBAAmB;IAO7D,qEAAqE;IACrE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAE5C,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;YAMpB,WAAW;IAmCzB;;;OAGG;IACH,OAAO,CAAC,eAAe;IAuBvB,OAAO,CAAC,WAAW;IAmBnB,gBAAgB;IAShB,OAAO,CAAC,OAAO;IAgBf,OAAO,CAAC,OAAO;CAoBhB"}
|
|
1
|
+
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAO5D,KAAK,0BAA0B,GAAG;IAChC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,8BAA8B,CAAC;IACrC,WAAW,EAAE,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAC7E,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAEF,qBAAa,oBAAoB;IAM7B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAP1B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IACxD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA6B;IAChE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;gBAGjD,cAAc,EAAE,MAAM,EACtB,aAAa,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,0BAA0B,YAAA;IAKtD,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIpD,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAInC,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE;IAU3C,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAQrD,iBAAiB,CAAC,gBAAgB,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAYrE,WAAW,CAAC,UAAU,EAAE,MAAM;IAU9B,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAI/D,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB;IAsB/D,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,mBAAmB;IAO7D,qEAAqE;IACrE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAE5C,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;YAMpB,WAAW;IAmCzB;;;OAGG;IACH,OAAO,CAAC,eAAe;IAuBvB,OAAO,CAAC,WAAW;IAmBnB,gBAAgB;IAShB,OAAO,CAAC,OAAO;IAgBf,OAAO,CAAC,OAAO;CAoBhB"}
|
package/dist/session-manager.js
CHANGED
|
@@ -17,6 +17,9 @@ export class ClaudeSessionManager {
|
|
|
17
17
|
getSessionId(sessionKey) {
|
|
18
18
|
return this.sessionIds.get(sessionKey);
|
|
19
19
|
}
|
|
20
|
+
isMain(sessionKey) {
|
|
21
|
+
return sessionKey === this.mainSessionKey;
|
|
22
|
+
}
|
|
20
23
|
getResumeArgs(sessionKey) {
|
|
21
24
|
const existing = this.sessionIds.get(sessionKey);
|
|
22
25
|
if (existing)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { GatewayLogger, RuntimeEvent } from '@parall/agent-core';
|
|
2
|
+
import { AsyncQueue } from '@parall/agent-core/internal/async-queue';
|
|
3
|
+
import type { ClaudeResultMeta } from './output-parser.js';
|
|
4
|
+
/**
|
|
5
|
+
* Why a turn ended from the pump's point of view. `result` and
|
|
6
|
+
* `error_result` are the CLI's own boundary; `absorbed` means a dispatch
|
|
7
|
+
* delivery started mid runtime-initiated turn and took over the tail; the
|
|
8
|
+
* rest are bridge-side terminations (process EOF / kill, gateway detach,
|
|
9
|
+
* dispatch abort).
|
|
10
|
+
*/
|
|
11
|
+
export type ClaudeTurnEndReason = 'result' | 'error_result' | 'absorbed' | 'eof' | 'killed' | 'aborted' | 'detached';
|
|
12
|
+
export type ClaudeTurnEnvelope = {
|
|
13
|
+
kind: 'runtime';
|
|
14
|
+
event: RuntimeEvent;
|
|
15
|
+
}
|
|
16
|
+
/** The delivery's lifecycle reached a terminal state (dispatch drains only). */
|
|
17
|
+
| {
|
|
18
|
+
kind: 'terminal';
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'ended';
|
|
21
|
+
reason: ClaudeTurnEndReason;
|
|
22
|
+
message?: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Turn-outcome evidence attached to whichever owner (dispatch delivery or
|
|
26
|
+
* runtime-initiated turn) the pump routes frames to
|
|
27
|
+
* (agent-turn-outcome-design.md §4.1): the LAST non-poison result frame seen
|
|
28
|
+
* while the owner held stdout, plus synthetic assistant notices.
|
|
29
|
+
*/
|
|
30
|
+
export type TurnEvidence = {
|
|
31
|
+
lastResultMeta?: ClaudeResultMeta;
|
|
32
|
+
/**
|
|
33
|
+
* The LAST `num_turns: 0` result frame in this owner's window: never turn
|
|
34
|
+
* evidence (the --resume poison frame looks the same), but a `/compact`
|
|
35
|
+
* command's own boundary — the idle auto-compact drain reads it.
|
|
36
|
+
*/
|
|
37
|
+
zeroTurnResultMeta?: ClaudeResultMeta;
|
|
38
|
+
/** `system/compact_boundary` seen in this owner's window (compact.ts evidence). */
|
|
39
|
+
compactBoundary?: {
|
|
40
|
+
preTokens?: number;
|
|
41
|
+
};
|
|
42
|
+
noticeTexts: string[];
|
|
43
|
+
sawError: boolean;
|
|
44
|
+
};
|
|
45
|
+
export declare function newTurnEvidence(): TurnEvidence;
|
|
46
|
+
/** Per-owner envelope queue between the stdout pump and the owner's drain. */
|
|
47
|
+
export type ClaudeTurnSink = AsyncQueue<ClaudeTurnEnvelope>;
|
|
48
|
+
export declare function newTurnSink(label: string, log?: GatewayLogger): ClaudeTurnSink;
|
|
49
|
+
//# sourceMappingURL=turn-sink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-sink.d.ts","sourceRoot":"","sources":["../src/turn-sink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AACrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAC3B,QAAQ,GACR,cAAc,GACd,UAAU,GACV,KAAK,GACL,QAAQ,GACR,SAAS,GACT,UAAU,CAAC;AAEf,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE;AAC1C,gFAAgF;GAC9E;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GACpB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,mBAAmB,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAErE;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,gBAAgB,CAAC;IACtC,mFAAmF;IACnF,eAAe,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,wBAAgB,eAAe,IAAI,YAAY,CAE9C;AAED,8EAA8E;AAC9E,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,kBAAkB,CAAC,CAAC;AAE5D,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,aAAa,GAAG,cAAc,CAK9E"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AsyncQueue } from '@parall/agent-core/internal/async-queue';
|
|
2
|
+
export function newTurnEvidence() {
|
|
3
|
+
return { noticeTexts: [], sawError: false };
|
|
4
|
+
}
|
|
5
|
+
export function newTurnSink(label, log) {
|
|
6
|
+
return new AsyncQueue({
|
|
7
|
+
onFirstDrop: () => log?.warn?.(`Claude turn sink ${label} exceeded its parked-frame cap; dropping oldest`),
|
|
8
|
+
});
|
|
9
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/claude-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.60.0",
|
|
4
4
|
"description": "Claude Code bridge runtime for self-hosted Parall agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"src"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@parall/agent-core": "1.
|
|
29
|
-
"@parall/cli": "1.
|
|
30
|
-
"@parall/sdk": "1.
|
|
28
|
+
"@parall/agent-core": "1.60.0",
|
|
29
|
+
"@parall/cli": "1.60.0",
|
|
30
|
+
"@parall/sdk": "1.60.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^22.0.0",
|