@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
package/dist/dispatch.js
CHANGED
|
@@ -2,9 +2,13 @@ import { execSync, spawn } from 'node:child_process';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
|
+
import { isRuntimeBusy, projectRuntimeEvent, RuntimeActivityPort } from '@parall/agent-core';
|
|
5
6
|
import { appendPreparedLocalAttachmentRefs, pinLocalAttachmentPaths, } from '@parall/agent-core/internal/attachment-input';
|
|
7
|
+
import { runClaudeCompact } from './compact.js';
|
|
8
|
+
import { aggregateBusyState, DEFAULT_FOLLOW_UP_HOLD_MS } from './busy-state.js';
|
|
6
9
|
import { ClaudeInputRegistry } from './input-lifecycle.js';
|
|
7
|
-
import { parseClaudeStreamJson
|
|
10
|
+
import { parseClaudeStreamJson } from './output-parser.js';
|
|
11
|
+
import { ClaudeProcessPump } from './process-pump.js';
|
|
8
12
|
import { buildSpawnEnv } from './spawn-env.js';
|
|
9
13
|
import { classifyClaudeTurn } from './turn-outcome.js';
|
|
10
14
|
export { buildSpawnEnv };
|
|
@@ -30,15 +34,19 @@ export class ClaudeCodeAdapter {
|
|
|
30
34
|
opts;
|
|
31
35
|
inputLifecycleMode = 'explicit';
|
|
32
36
|
processes = new Map();
|
|
37
|
+
/** Retired by abortDispatch while the CLI still had its own work: busy until idle, then terminated. */
|
|
38
|
+
retiring = new Set();
|
|
33
39
|
capabilityProbe;
|
|
34
40
|
capabilityProbeHandle;
|
|
35
41
|
shuttingDown = false;
|
|
42
|
+
activity;
|
|
36
43
|
_model;
|
|
37
44
|
_effortLevel;
|
|
38
45
|
_contextWindow;
|
|
39
46
|
_maxTokens;
|
|
40
47
|
constructor(opts) {
|
|
41
48
|
this.opts = opts;
|
|
49
|
+
this.activity = new RuntimeActivityPort('ClaudeCodeAdapter');
|
|
42
50
|
this._model = opts.model;
|
|
43
51
|
this._contextWindow = opts.contextWindow;
|
|
44
52
|
this._maxTokens = opts.maxTokens;
|
|
@@ -77,6 +85,24 @@ export class ClaudeCodeAdapter {
|
|
|
77
85
|
state.needsRestart = true;
|
|
78
86
|
}
|
|
79
87
|
}
|
|
88
|
+
// --- runtime-initiated work -------------------------------------------------
|
|
89
|
+
subscribeRuntimeActivity(handler) {
|
|
90
|
+
return this.activity.subscribe(handler);
|
|
91
|
+
}
|
|
92
|
+
busyState(now = this.now()) {
|
|
93
|
+
const states = [];
|
|
94
|
+
for (const state of this.processes.values()) {
|
|
95
|
+
if (state.done)
|
|
96
|
+
continue;
|
|
97
|
+
states.push(state.pump.busyState(now));
|
|
98
|
+
}
|
|
99
|
+
for (const state of this.retiring)
|
|
100
|
+
states.push(state.pump.busyState(now));
|
|
101
|
+
return aggregateBusyState(states);
|
|
102
|
+
}
|
|
103
|
+
isBusy(now = this.now()) {
|
|
104
|
+
return isRuntimeBusy(this.busyState(now), now);
|
|
105
|
+
}
|
|
80
106
|
enqueueDuringDispatch(sessionKey, body, inputLifecycle) {
|
|
81
107
|
// Exact identity is mandatory for soft steer. Without it the later
|
|
82
108
|
// buffered dispatch could only guess which stdout boundary belonged to
|
|
@@ -88,7 +114,7 @@ export class ClaudeCodeAdapter {
|
|
|
88
114
|
return false;
|
|
89
115
|
// A throwaway process already proved the CLI capability before any
|
|
90
116
|
// business process started. The real process revalidates its own init.
|
|
91
|
-
if (!state.capabilities
|
|
117
|
+
if (!state.pump.capabilities.has('msg_lifecycle_v1'))
|
|
92
118
|
return false;
|
|
93
119
|
const { proc } = state.handle;
|
|
94
120
|
if (proc.exitCode !== null || proc.signalCode !== null || proc.stdin.destroyed)
|
|
@@ -113,17 +139,23 @@ export class ClaudeCodeAdapter {
|
|
|
113
139
|
const state = this.processes.get(sessionKey);
|
|
114
140
|
if (!state || state.done)
|
|
115
141
|
return;
|
|
116
|
-
|
|
117
|
-
if (!delivery.terminal)
|
|
118
|
-
void state.inputs.failBestEffort(delivery);
|
|
119
|
-
}
|
|
142
|
+
state.pump.abortDeliveries('dispatch aborted');
|
|
120
143
|
state.done = true;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
144
|
+
if (state.pump.hasOwnWork()) {
|
|
145
|
+
// The CLI has (or is about to start) a runtime-initiated turn: retire
|
|
146
|
+
// the process instead of ending stdin under it. Out of `processes` and
|
|
147
|
+
// the session manager so the next dispatch's fresh process does not
|
|
148
|
+
// SIGTERM it as replaced; terminated once the pump reports idle.
|
|
149
|
+
this.processes.delete(sessionKey);
|
|
150
|
+
this.opts.sessionManager.clearProcess(sessionKey, state.handle);
|
|
151
|
+
this.retiring.add(state);
|
|
152
|
+
state.pump.whenIdle(() => {
|
|
153
|
+
if (this.retiring.delete(state))
|
|
154
|
+
this.terminateHandle(state.handle);
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
126
157
|
}
|
|
158
|
+
this.endStdin(state.handle);
|
|
127
159
|
}
|
|
128
160
|
hasPendingInjections(sessionKey) {
|
|
129
161
|
const state = this.processes.get(sessionKey);
|
|
@@ -131,6 +163,23 @@ export class ClaudeCodeAdapter {
|
|
|
131
163
|
return false;
|
|
132
164
|
return state.inputs.hasPendingInjections();
|
|
133
165
|
}
|
|
166
|
+
hasUnsettledInjections(sessionKey) {
|
|
167
|
+
const state = this.processes.get(sessionKey);
|
|
168
|
+
if (!state)
|
|
169
|
+
return false;
|
|
170
|
+
return state.inputs.hasUnsettledInjections();
|
|
171
|
+
}
|
|
172
|
+
acknowledgeDiscardedInjection(sessionKey, deliveryKey) {
|
|
173
|
+
const state = this.processes.get(sessionKey);
|
|
174
|
+
if (!state)
|
|
175
|
+
return;
|
|
176
|
+
state.inputs.discardBookkeeping(deliveryKey);
|
|
177
|
+
// The discarded copy may have been the last thing a lazy restart was
|
|
178
|
+
// waiting on. Unlike the consume path's tail, the discarded delivery may
|
|
179
|
+
// still be RUNNING (non-terminal): killing the process now would fail a
|
|
180
|
+
// live input, so the restart also waits for every injection to settle.
|
|
181
|
+
this.maybeApplyRestart(sessionKey, state);
|
|
182
|
+
}
|
|
134
183
|
async *dispatch({ event, bodyForAgent, sessionKey, context, inputLifecycle, noteActivity, }) {
|
|
135
184
|
const deliveryKey = inputLifecycle?.deliveryKey ?? event.dispatchEventId ?? event.messageId;
|
|
136
185
|
const existingState = this.processes.get(sessionKey);
|
|
@@ -145,14 +194,12 @@ export class ClaudeCodeAdapter {
|
|
|
145
194
|
injected.drained = true;
|
|
146
195
|
context.log?.info?.(`consuming steer input ${injected.commandUuid}`);
|
|
147
196
|
try {
|
|
148
|
-
yield* this.consumeDelivery(sessionKey, existingState, injected,
|
|
197
|
+
yield* this.consumeDelivery(sessionKey, existingState, injected, noteActivity);
|
|
149
198
|
}
|
|
150
199
|
finally {
|
|
151
200
|
existingState.inputs.remove(injected);
|
|
152
201
|
}
|
|
153
|
-
|
|
154
|
-
this.killProcess(sessionKey, existingState);
|
|
155
|
-
}
|
|
202
|
+
this.maybeApplyRestart(sessionKey, existingState);
|
|
156
203
|
return;
|
|
157
204
|
}
|
|
158
205
|
}
|
|
@@ -208,6 +255,12 @@ export class ClaudeCodeAdapter {
|
|
|
208
255
|
this.killProcess(sessionKey, state);
|
|
209
256
|
}
|
|
210
257
|
this.processes.clear();
|
|
258
|
+
const retired = [...this.retiring];
|
|
259
|
+
this.retiring.clear();
|
|
260
|
+
for (const state of retired) {
|
|
261
|
+
state.pump.close('killed', 'Claude process terminated by the bridge');
|
|
262
|
+
this.terminateHandle(state.handle);
|
|
263
|
+
}
|
|
211
264
|
}
|
|
212
265
|
async shutdown() {
|
|
213
266
|
this.shuttingDown = true;
|
|
@@ -218,6 +271,9 @@ export class ClaudeCodeAdapter {
|
|
|
218
271
|
this.resetProcesses();
|
|
219
272
|
await this.opts.sessionManager.shutdownAll();
|
|
220
273
|
}
|
|
274
|
+
now() {
|
|
275
|
+
return (this.opts.now ?? Date.now)();
|
|
276
|
+
}
|
|
221
277
|
async *runTurn(sessionKey, promptBody, deliveryKey, lifecycle, log, noteActivity) {
|
|
222
278
|
let state;
|
|
223
279
|
try {
|
|
@@ -234,7 +290,9 @@ export class ClaudeCodeAdapter {
|
|
|
234
290
|
yield { type: 'error', message: `Claude spawn failed: ${String(err)}` };
|
|
235
291
|
return;
|
|
236
292
|
}
|
|
237
|
-
|
|
293
|
+
// Registered BEFORE the stdin write: the pump may route this command's
|
|
294
|
+
// queued/started frames before the drain below starts.
|
|
295
|
+
const delivery = state.inputs.register(deliveryKey, lifecycle, false, noteActivity, log);
|
|
238
296
|
try {
|
|
239
297
|
this.writeUserMessage(state.handle, promptBody, delivery.commandUuid);
|
|
240
298
|
}
|
|
@@ -246,12 +304,22 @@ export class ClaudeCodeAdapter {
|
|
|
246
304
|
return;
|
|
247
305
|
}
|
|
248
306
|
try {
|
|
249
|
-
yield* this.consumeDelivery(sessionKey, state, delivery,
|
|
307
|
+
yield* this.consumeDelivery(sessionKey, state, delivery, noteActivity);
|
|
250
308
|
}
|
|
251
309
|
finally {
|
|
252
310
|
state.inputs.remove(delivery);
|
|
253
311
|
}
|
|
254
312
|
}
|
|
313
|
+
/** Idle auto-compact (compact.ts): a `/compact` frame into the long-lived process. */
|
|
314
|
+
compact(opts) {
|
|
315
|
+
return runClaudeCompact({
|
|
316
|
+
ensureRuntimeCapability: (log) => this.ensureRuntimeCapability(log),
|
|
317
|
+
ensureProcess: (sessionKey, log) => this.ensureProcess(sessionKey, log),
|
|
318
|
+
killProcess: (sessionKey, state) => this.killProcess(sessionKey, state),
|
|
319
|
+
writeUserMessage: (handle, text, uuid) => this.writeUserMessage(handle, text, uuid),
|
|
320
|
+
applyPendingRestart: (sessionKey, state) => this.maybeApplyRestart(sessionKey, state),
|
|
321
|
+
}, opts);
|
|
322
|
+
}
|
|
255
323
|
ensureRuntimeCapability(log) {
|
|
256
324
|
if (!this.capabilityProbe) {
|
|
257
325
|
const probe = this.probeRuntimeCapability(log);
|
|
@@ -308,176 +376,79 @@ export class ClaudeCodeAdapter {
|
|
|
308
376
|
}
|
|
309
377
|
}
|
|
310
378
|
/**
|
|
311
|
-
* Drain the
|
|
312
|
-
* reaches a terminal lifecycle state. Other injected inputs may start
|
|
313
|
-
* finish while this drain is active; their callbacks advance
|
|
314
|
-
* and their later bookkeeping dispatch becomes a no-op.
|
|
379
|
+
* Drain the envelopes the pump routed to `target` until its exact stdin
|
|
380
|
+
* UUID reaches a terminal lifecycle state. Other injected inputs may start
|
|
381
|
+
* and finish while this drain is active; their callbacks advance in the
|
|
382
|
+
* pump independently and their later bookkeeping dispatch becomes a no-op.
|
|
315
383
|
*/
|
|
316
|
-
async *consumeDelivery(sessionKey, state, target,
|
|
384
|
+
async *consumeDelivery(sessionKey, state, target, noteActivity) {
|
|
317
385
|
if (target.terminal)
|
|
318
386
|
return;
|
|
319
387
|
const groupKey = randomUUID();
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
// (num_turns: 0 startup artifacts) never overwrite evidence. At most one
|
|
326
|
-
// turn_outcome is emitted per dispatch, right before the generator
|
|
327
|
-
// returns; with no evidence and no crash, nothing is emitted and the
|
|
328
|
-
// legacy boolean error path stands (refine-only, never guess).
|
|
329
|
-
let lastResultMeta;
|
|
330
|
-
const noticeTexts = [];
|
|
331
|
-
// usage_limit settles via the lane-level deferred complete; a failed-input
|
|
332
|
-
// report would race it with an immediate redrive into the choked LLM.
|
|
333
|
-
const settledAsLimit = () => classifyClaudeTurn(lastResultMeta, noticeTexts).outcome === 'usage_limit';
|
|
334
|
-
while (!target.terminal) {
|
|
335
|
-
const next = await state.parser.next();
|
|
336
|
-
if (next.done) {
|
|
337
|
-
state.done = true;
|
|
338
|
-
this.processes.delete(sessionKey);
|
|
339
|
-
const detail = state.handle.stderrChunks.join('').trim();
|
|
340
|
-
const exit = await state.handle.exitPromise.catch(() => ({ code: null, signal: null }));
|
|
341
|
-
if (detail) {
|
|
342
|
-
log?.warn?.(`subprocess stderr: ${detail}`);
|
|
343
|
-
}
|
|
344
|
-
if (settledAsLimit())
|
|
345
|
-
target.suppressFailReport = true;
|
|
346
|
-
await state.inputs.failBestEffort(target, log);
|
|
347
|
-
if (!sawError) {
|
|
348
|
-
yield {
|
|
349
|
-
type: 'error',
|
|
350
|
-
message: detail ||
|
|
351
|
-
`Claude exited with code ${exit.code ?? 'unknown'}${exit.signal ? ` (${exit.signal})` : ''}`,
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
// No evidence at all classifies as runtime_crash (the process died
|
|
355
|
-
// before any result frame); with evidence, classify what we saw.
|
|
356
|
-
yield classifyClaudeTurn(lastResultMeta, noticeTexts);
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
const parsed = next.value;
|
|
360
|
-
// Count every parser-observed CLI progress frame as activity, including
|
|
361
|
-
// command lifecycle, turn boundaries, and explicit activity markers
|
|
362
|
-
// for nested subagent frames that must not become RuntimeEvents.
|
|
363
|
-
noteActivity?.();
|
|
364
|
-
if (parsed.type === 'runtime_activity')
|
|
365
|
-
continue;
|
|
366
|
-
if (parsed.type === 'runtime_init') {
|
|
367
|
-
state.capabilities = new Set(parsed.capabilities);
|
|
368
|
-
if (parsed.sessionId) {
|
|
369
|
-
this.opts.sessionManager.recordSessionId(sessionKey, parsed.sessionId);
|
|
370
|
-
yield {
|
|
371
|
-
type: 'runtime_session',
|
|
372
|
-
runtimeSessionId: parsed.sessionId,
|
|
373
|
-
runtimeLaneKey: sessionKey,
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
if (!state.capabilities.has('msg_lifecycle_v1')) {
|
|
377
|
-
await state.inputs.failBestEffort(target, log);
|
|
378
|
-
yield {
|
|
379
|
-
type: 'error',
|
|
380
|
-
message: 'Claude runtime lacks required msg_lifecycle_v1 capability; refusing heuristic input coverage',
|
|
381
|
-
};
|
|
382
|
-
this.killProcess(sessionKey, state);
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
if (parsed.type === 'command_lifecycle') {
|
|
388
|
-
if (!state.capabilities?.has('msg_lifecycle_v1')) {
|
|
389
|
-
await state.inputs.failBestEffort(target, log);
|
|
390
|
-
yield {
|
|
391
|
-
type: 'error',
|
|
392
|
-
message: 'Claude emitted command lifecycle before advertising msg_lifecycle_v1',
|
|
393
|
-
};
|
|
394
|
-
this.killProcess(sessionKey, state);
|
|
388
|
+
state.pump.setActiveDrain(target, noteActivity);
|
|
389
|
+
try {
|
|
390
|
+
while (true) {
|
|
391
|
+
const next = await target.sink.next();
|
|
392
|
+
if (next.done)
|
|
395
393
|
return;
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
log?.warn?.(`ignoring lifecycle for unknown Claude command ${parsed.commandUuid}`);
|
|
394
|
+
const envelope = next.value;
|
|
395
|
+
if (envelope.kind === 'runtime') {
|
|
396
|
+
yield projectRuntimeEvent(envelope.event, groupKey);
|
|
400
397
|
continue;
|
|
401
398
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
399
|
+
if (envelope.kind === 'terminal')
|
|
400
|
+
break;
|
|
401
|
+
// The process is gone (eof / killed), the dispatch was aborted, or
|
|
402
|
+
// the pump ended this owner's window: surface the error unless the
|
|
403
|
+
// stream already carried one, then classify what was observed (no
|
|
404
|
+
// evidence at all is a runtime_crash).
|
|
405
|
+
if (envelope.reason !== 'error_result' && !target.evidence.sawError) {
|
|
407
406
|
yield {
|
|
408
407
|
type: 'error',
|
|
409
|
-
message: `Claude
|
|
408
|
+
message: envelope.message ?? `Claude turn ${envelope.reason}`,
|
|
410
409
|
};
|
|
411
|
-
this.killProcess(sessionKey, state);
|
|
412
|
-
return;
|
|
413
410
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (parsed.type === 'turn_end') {
|
|
417
|
-
// Evidence capture: the poison startup frame (numTurns === 0) is a
|
|
418
|
-
// resume artifact, not a turn boundary — never let it overwrite real
|
|
419
|
-
// evidence.
|
|
420
|
-
if (parsed.numTurns !== 0) {
|
|
421
|
-
lastResultMeta = parsed.resultMeta;
|
|
422
|
-
}
|
|
423
|
-
if (parsed.isError) {
|
|
424
|
-
const limitSettled = settledAsLimit();
|
|
425
|
-
const failedDelivery = parsed.userMessageUuid
|
|
426
|
-
? state.inputs.getByCommand(parsed.userMessageUuid)
|
|
427
|
-
: undefined;
|
|
428
|
-
if (!failedDelivery) {
|
|
429
|
-
if (limitSettled) {
|
|
430
|
-
for (const delivery of state.inputs.values()) {
|
|
431
|
-
delivery.suppressFailReport = true;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
await state.inputs.failAllBestEffort(log);
|
|
435
|
-
yield classifyClaudeTurn(lastResultMeta, noticeTexts);
|
|
436
|
-
this.killProcess(sessionKey, state);
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
failedDelivery.resultFailed = true;
|
|
440
|
-
if (limitSettled)
|
|
441
|
-
failedDelivery.suppressFailReport = true;
|
|
442
|
-
}
|
|
443
|
-
// result adjacency is not a consumption boundary: the matching
|
|
444
|
-
// command_lifecycle(completed) may follow it or interleave with a
|
|
445
|
-
// different queued/injected command.
|
|
446
|
-
continue;
|
|
447
|
-
}
|
|
448
|
-
if (parsed.type === 'assistant_error') {
|
|
449
|
-
noticeTexts.push(parsed.message);
|
|
450
|
-
continue;
|
|
451
|
-
}
|
|
452
|
-
if (parsed.type === 'error') {
|
|
453
|
-
sawError = true;
|
|
454
|
-
yield parsed;
|
|
455
|
-
continue;
|
|
456
|
-
}
|
|
457
|
-
if (parsed.type === 'text') {
|
|
458
|
-
yield { ...parsed, project: false, groupKey };
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
|
-
if (parsed.type === 'runtime_session') {
|
|
462
|
-
yield parsed;
|
|
463
|
-
continue;
|
|
411
|
+
yield classifyClaudeTurn(target.evidence.lastResultMeta, target.evidence.noticeTexts);
|
|
412
|
+
return;
|
|
464
413
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
414
|
+
// Delivery reached a lifecycle terminal. Classify only when a result
|
|
415
|
+
// frame was observed in this owner's window: a delivery whose frames
|
|
416
|
+
// were drained by a sibling consumption window has no evidence here,
|
|
417
|
+
// and guessing would mislabel the turn (refine-only rule, §4.3).
|
|
418
|
+
if (target.evidence.lastResultMeta) {
|
|
419
|
+
yield classifyClaudeTurn(target.evidence.lastResultMeta, target.evidence.noticeTexts);
|
|
468
420
|
}
|
|
469
|
-
yield { ...parsed, groupKey };
|
|
470
421
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
// guessing would mislabel the turn (refine-only rule, §4.3).
|
|
475
|
-
if (lastResultMeta) {
|
|
476
|
-
yield classifyClaudeTurn(lastResultMeta, noticeTexts);
|
|
422
|
+
finally {
|
|
423
|
+
state.pump.clearActiveDrain(target);
|
|
424
|
+
this.maybeApplyRestart(sessionKey, state);
|
|
477
425
|
}
|
|
478
|
-
|
|
479
|
-
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Lazy restart: kill the process so the next dispatch respawns it (the
|
|
429
|
+
* session survives via --resume). Waits for every injection to settle, for
|
|
430
|
+
* an open runtime-initiated turn and for a follow-up hold — but NOT for
|
|
431
|
+
* outstanding background tasks (a `make dev` would defer a config change
|
|
432
|
+
* forever); those die with the process, logged.
|
|
433
|
+
*/
|
|
434
|
+
maybeApplyRestart(sessionKey, state) {
|
|
435
|
+
if (!state.needsRestart || state.done)
|
|
436
|
+
return;
|
|
437
|
+
if (state.inputs.hasPendingInjections() || state.inputs.hasUnsettledInjections())
|
|
438
|
+
return;
|
|
439
|
+
// A dispatch queued behind a runtime-initiated turn is registered but
|
|
440
|
+
// not started yet; killing now would fail it before the CLI runs it.
|
|
441
|
+
for (const delivery of state.inputs.values()) {
|
|
442
|
+
if (!delivery.terminal)
|
|
443
|
+
return;
|
|
480
444
|
}
|
|
445
|
+
if (state.pump.hasOwnWork())
|
|
446
|
+
return;
|
|
447
|
+
const outstanding = state.pump.busy.outstanding();
|
|
448
|
+
if (outstanding.total > 0) {
|
|
449
|
+
state.log?.warn?.(`restarting ${sessionKey} with ${outstanding.total} live background task(s); they die with the process`);
|
|
450
|
+
}
|
|
451
|
+
this.killProcess(sessionKey, state);
|
|
481
452
|
}
|
|
482
453
|
ensureProcess(sessionKey, log) {
|
|
483
454
|
if (this.shuttingDown) {
|
|
@@ -486,10 +457,15 @@ export class ClaudeCodeAdapter {
|
|
|
486
457
|
const existing = this.processes.get(sessionKey);
|
|
487
458
|
if (existing && !existing.done) {
|
|
488
459
|
const { proc } = existing.handle;
|
|
489
|
-
|
|
460
|
+
const alive = proc.exitCode === null && proc.signalCode === null && !proc.stdin.destroyed;
|
|
461
|
+
if (existing.needsRestart && alive) {
|
|
462
|
+
if (existing.pump.hasOwnWork()) {
|
|
463
|
+
log?.info?.(`deferring lazy restart of ${sessionKey}: runtime-initiated turn in progress`);
|
|
464
|
+
return existing;
|
|
465
|
+
}
|
|
490
466
|
this.killProcess(sessionKey, existing);
|
|
491
467
|
}
|
|
492
|
-
else if (
|
|
468
|
+
else if (alive) {
|
|
493
469
|
return existing;
|
|
494
470
|
}
|
|
495
471
|
else {
|
|
@@ -498,19 +474,52 @@ export class ClaudeCodeAdapter {
|
|
|
498
474
|
}
|
|
499
475
|
const handle = this.spawnProcess(sessionKey, log);
|
|
500
476
|
const parser = parseClaudeStreamJson(handle.proc.stdout);
|
|
477
|
+
const inputs = new ClaudeInputRegistry();
|
|
501
478
|
const state = {
|
|
502
479
|
handle,
|
|
503
|
-
|
|
480
|
+
pump: new ClaudeProcessPump({
|
|
481
|
+
sessionKey,
|
|
482
|
+
parser,
|
|
483
|
+
handle,
|
|
484
|
+
inputs,
|
|
485
|
+
log,
|
|
486
|
+
followUpHoldMs: this.opts.followUpHoldMs ?? DEFAULT_FOLLOW_UP_HOLD_MS,
|
|
487
|
+
now: () => this.now(),
|
|
488
|
+
hooks: {
|
|
489
|
+
onRuntimeInit: (init) => {
|
|
490
|
+
if (init.sessionId) {
|
|
491
|
+
this.opts.sessionManager.recordSessionId(sessionKey, init.sessionId);
|
|
492
|
+
}
|
|
493
|
+
if (!init.capabilities.includes('msg_lifecycle_v1')) {
|
|
494
|
+
return 'Claude runtime lacks required msg_lifecycle_v1 capability; refusing heuristic input coverage';
|
|
495
|
+
}
|
|
496
|
+
return undefined;
|
|
497
|
+
},
|
|
498
|
+
onFatal: () => {
|
|
499
|
+
const current = this.processes.get(sessionKey);
|
|
500
|
+
if (current === state)
|
|
501
|
+
this.killProcess(sessionKey, state);
|
|
502
|
+
},
|
|
503
|
+
onEof: () => {
|
|
504
|
+
state.done = true;
|
|
505
|
+
if (this.processes.get(sessionKey) === state)
|
|
506
|
+
this.processes.delete(sessionKey);
|
|
507
|
+
this.retiring.delete(state);
|
|
508
|
+
},
|
|
509
|
+
// Main-session turns go to the gateway (steps + session activity);
|
|
510
|
+
// fork-session turns only count as busy (fork-scope invariant).
|
|
511
|
+
onRuntimeTurnOpened: (turn) => this.activity.surfaceTurn(turn, this.opts.sessionManager.isMain(sessionKey), log),
|
|
512
|
+
onRuntimeTurnClosed: () => this.maybeApplyRestart(sessionKey, state),
|
|
513
|
+
},
|
|
514
|
+
}),
|
|
504
515
|
done: false,
|
|
505
516
|
needsRestart: false,
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
// so pre-seed the gate and still verify the real init when it arrives.
|
|
509
|
-
capabilities: new Set(['msg_lifecycle_v1']),
|
|
510
|
-
inputs: new ClaudeInputRegistry(),
|
|
517
|
+
inputs,
|
|
518
|
+
log,
|
|
511
519
|
};
|
|
512
520
|
this.processes.set(sessionKey, state);
|
|
513
521
|
this.opts.sessionManager.registerProcess(sessionKey, handle);
|
|
522
|
+
state.pump.start();
|
|
514
523
|
return state;
|
|
515
524
|
}
|
|
516
525
|
spawnProcess(sessionKey, log, resume = true) {
|
|
@@ -561,15 +570,19 @@ export class ClaudeCodeAdapter {
|
|
|
561
570
|
if (current === state) {
|
|
562
571
|
this.processes.delete(sessionKey);
|
|
563
572
|
}
|
|
573
|
+
state.pump.close('killed', 'Claude process terminated by the bridge');
|
|
564
574
|
this.terminateHandle(state.handle);
|
|
565
575
|
}
|
|
566
|
-
|
|
576
|
+
endStdin(handle) {
|
|
567
577
|
try {
|
|
568
578
|
handle.proc.stdin.end();
|
|
569
579
|
}
|
|
570
580
|
catch {
|
|
571
581
|
/* best-effort */
|
|
572
582
|
}
|
|
583
|
+
}
|
|
584
|
+
terminateHandle(handle) {
|
|
585
|
+
this.endStdin(handle);
|
|
573
586
|
if (handle.proc.exitCode === null && handle.proc.signalCode === null) {
|
|
574
587
|
try {
|
|
575
588
|
if (!IS_WIN32 || !handle.proc.pid || !killWin32Tree(handle.proc.pid)) {
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as os from 'node:os';
|
|
3
|
-
import { capabilityBinDir, childLogger, clearAllProviderCreds, configureHttpKeepAlive, createLogger, createOtelLogger, createPlatformConfigManager, deriveModelIsPin, identityFromMe, initAgentTelemetry, llmSource, materializeChannelCapabilities, ParallAgentGateway, parseDispatchDeadlineMs, parseForkDeadlineMs, parseProviderConfig, parseShutdownDeadlineMs, resolveRuntimeContextWindow, resolveRuntimeMaxTokens, resolveRuntimeModel, } from '@parall/agent-core';
|
|
3
|
+
import { capabilityBinDir, childLogger, clearAllProviderCreds, configureHttpKeepAlive, createLogger, createOtelLogger, createPlatformConfigManager, deriveModelIsPin, identityFromMe, initAgentTelemetry, llmSource, resolveServiceVersion, materializeChannelCapabilities, ParallAgentGateway, parseDispatchDeadlineMs, parseForkDeadlineMs, parseProviderConfig, parseShutdownDeadlineMs, resolveRuntimeContextWindow, resolveRuntimeMaxTokens, resolveRuntimeModel, } from '@parall/agent-core';
|
|
4
4
|
import { ApiError, ParallClient, ParallWs } from '@parall/sdk';
|
|
5
5
|
import { buildClaudeRuntimeKey, contextFilePathForSession, dispatchContextDirPath, resolveClaudeAgentConfig, resolveWsUrl, sessionStateFilePathForRuntime, stepIdFilePathForSession, } from './config.js';
|
|
6
6
|
import { ClaudeCodeAdapter } from './dispatch.js';
|
|
@@ -44,7 +44,11 @@ function resolveProviderEnv() {
|
|
|
44
44
|
async function main() {
|
|
45
45
|
// Before any fetch: long-lived HTTP connections for every bridge→api call.
|
|
46
46
|
configureHttpKeepAlive();
|
|
47
|
-
const telemetry = await initAgentTelemetry('parall-claude-agent', 'claude-code'
|
|
47
|
+
const telemetry = await initAgentTelemetry('parall-claude-agent', 'claude-code', {
|
|
48
|
+
apiUrl: process.env.PRLL_API_URL,
|
|
49
|
+
apiKey: process.env.PRLL_API_KEY,
|
|
50
|
+
serviceVersion: resolveServiceVersion(import.meta.url),
|
|
51
|
+
});
|
|
48
52
|
activeLog = createOtelLogger('agent', 'claude-agent');
|
|
49
53
|
try {
|
|
50
54
|
const activeLLMSource = resolveProviderEnv();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DispatchInputLifecycle, GatewayLogger, RuntimeInputState } from '@parall/agent-core';
|
|
2
|
+
import { type ClaudeTurnSink, type TurnEvidence } from './turn-sink.js';
|
|
2
3
|
export type ClaudeInputDelivery = {
|
|
3
4
|
deliveryKey: string;
|
|
4
5
|
commandUuid: string;
|
|
@@ -8,6 +9,14 @@ export type ClaudeInputDelivery = {
|
|
|
8
9
|
terminal?: 'completed' | 'failed' | 'settled';
|
|
9
10
|
reportedState?: RuntimeInputState;
|
|
10
11
|
resultFailed: boolean;
|
|
12
|
+
/** Envelopes the stdout pump routed to this delivery; drained by its dispatch. */
|
|
13
|
+
sink: ClaudeTurnSink;
|
|
14
|
+
/** Turn-outcome evidence gathered while this delivery owned stdout. */
|
|
15
|
+
evidence: TurnEvidence;
|
|
16
|
+
/** Dispatch inactivity signal of the dispatch draining (or about to drain) this delivery. */
|
|
17
|
+
noteActivity?: () => void;
|
|
18
|
+
/** runtime_session already pushed to this delivery's sink. */
|
|
19
|
+
sessionAnnounced: boolean;
|
|
11
20
|
/**
|
|
12
21
|
* The turn settled as usage_limit: the lane-level deferred complete will
|
|
13
22
|
* re-deliver this input at retry_at without burning redrive budget. A
|
|
@@ -30,8 +39,29 @@ export declare class ClaudeInputRegistry {
|
|
|
30
39
|
findOverlapping(dispatchEventIds: string[]): ClaudeInputDelivery | undefined;
|
|
31
40
|
values(): IterableIterator<ClaudeInputDelivery>;
|
|
32
41
|
hasPendingInjections(): boolean;
|
|
33
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Injected inputs whose lifecycle has NOT reached a terminal state. A
|
|
44
|
+
* terminal delivery still counts as pending (its buffered copy owes frame
|
|
45
|
+
* boundary bookkeeping) but owes the server nothing — the lane complete
|
|
46
|
+
* defers on this predicate, never on hasPendingInjections, so a bookkeeping
|
|
47
|
+
* copy that never drains cannot hold the lane open past its lease. Drained
|
|
48
|
+
* is deliberately NOT part of this predicate: a discarded bookkeeping copy
|
|
49
|
+
* (drained, non-terminal) is still a running input — completing its lane
|
|
50
|
+
* would release the member for redrive mid-processing.
|
|
51
|
+
*/
|
|
52
|
+
hasUnsettledInjections(): boolean;
|
|
53
|
+
register(deliveryKey: string, lifecycle: DispatchInputLifecycle | undefined, injected: boolean, noteActivity?: () => void, log?: GatewayLogger): ClaudeInputDelivery;
|
|
34
54
|
remove(delivery: ClaudeInputDelivery): void;
|
|
55
|
+
/**
|
|
56
|
+
* The buffered copy backing this delivery was discarded without a
|
|
57
|
+
* dispatch (already rendered into a frame the model saw), so the normal
|
|
58
|
+
* bookkeeping drain never comes. Its frame-boundary obligation is void:
|
|
59
|
+
* mark it drained so nothing waits on it, and drop it once terminal. A
|
|
60
|
+
* still-running input keeps its registration — byCommand must keep routing
|
|
61
|
+
* its lifecycle frames — and is dropped when its terminal state arrives
|
|
62
|
+
* (see apply/fail).
|
|
63
|
+
*/
|
|
64
|
+
discardBookkeeping(deliveryKey: string): void;
|
|
35
65
|
apply(delivery: ClaudeInputDelivery, state: 'queued' | 'started' | 'completed' | 'cancelled' | 'discarded'): Promise<void>;
|
|
36
66
|
fail(delivery: ClaudeInputDelivery): Promise<void>;
|
|
37
67
|
failBestEffort(delivery: ClaudeInputDelivery, log?: GatewayLogger): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input-lifecycle.d.ts","sourceRoot":"","sources":["../src/input-lifecycle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"input-lifecycle.d.ts","sourceRoot":"","sources":["../src/input-lifecycle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACnG,OAAO,EACL,KAAK,cAAc,EAGnB,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC9C,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,YAAY,EAAE,OAAO,CAAC;IACtB,kFAAkF;IAClF,IAAI,EAAE,cAAc,CAAC;IACrB,uEAAuE;IACvE,QAAQ,EAAE,YAAY,CAAC;IACvB,6FAA6F;IAC7F,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,8DAA8D;IAC9D,gBAAgB,EAAE,OAAO,CAAC;IAC1B;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAEF;;;;GAIG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0C;IAChE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;IAEpE,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAI9D,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAIlE,eAAe,CAAC,gBAAgB,EAAE,MAAM,EAAE,GAAG,mBAAmB,GAAG,SAAS;IAU5E,MAAM,IAAI,gBAAgB,CAAC,mBAAmB,CAAC;IAI/C,oBAAoB,IAAI,OAAO;IAI/B;;;;;;;;;OASG;IACH,sBAAsB,IAAI,OAAO;IAIjC,QAAQ,CACN,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,sBAAsB,GAAG,SAAS,EAC7C,QAAQ,EAAE,OAAO,EACjB,YAAY,CAAC,EAAE,MAAM,IAAI,EACzB,GAAG,CAAC,EAAE,aAAa,GAClB,mBAAmB;IAsBtB,MAAM,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAY3C;;;;;;;;OAQG;IACH,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAOvC,KAAK,CACT,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACpE,OAAO,CAAC,IAAI,CAAC;IA0BV,IAAI,CAAC,QAAQ,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBlD,cAAc,CAAC,QAAQ,EAAE,mBAAmB,EAAE,GAAG,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAUjF,iBAAiB,CAAC,GAAG,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;CAO5D"}
|