@parall/claude-agent 1.45.0 → 1.47.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/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +36 -0
- package/dist/index.js +3 -1
- package/dist/output-parser.d.ts +1 -0
- package/dist/output-parser.d.ts.map +1 -1
- package/dist/output-parser.js +5 -0
- package/package.json +4 -4
- package/src/dispatch.ts +44 -0
- package/src/index.ts +3 -0
- package/src/output-parser.ts +14 -1
package/dist/dispatch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,QAAQ,EAER,YAAY,EACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,OAAO,EAAE,oBAAoB,EAA4B,MAAM,sBAAsB,CAAC;AAEtF,KAAK,wBAAwB,GAAG,IAAI,CAClC,iBAAiB,EACf,gBAAgB,GAChB,aAAa,GACb,cAAc,GACd,oBAAoB,GACpB,WAAW,GACX,YAAY,GACZ,iBAAiB,GACjB,OAAO,GACP,gBAAgB,GAChB,cAAc,CACjB,GAAG;IACF,cAAc,EAAE,oBAAoB,CAAC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,yBAAyB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3D,iDAAiD;IACjD,wBAAwB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1D;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;
|
|
1
|
+
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
|
package/dist/dispatch.js
CHANGED
|
@@ -5,6 +5,10 @@ import { execSync, spawn } from 'node:child_process';
|
|
|
5
5
|
import { appendPreparedLocalAttachmentRefs, pinLocalAttachmentPaths, } from '@parall/agent-core/internal/attachment-input';
|
|
6
6
|
import { parseClaudeStreamJson } from './output-parser.js';
|
|
7
7
|
const IS_WIN32 = process.platform === 'win32';
|
|
8
|
+
// Upper bound on spurious empty turn_end frames skipped per turn (see the
|
|
9
|
+
// guard in runTurn) — keeps a pathological stream of empty `result` frames
|
|
10
|
+
// from spinning the drain loop forever.
|
|
11
|
+
const MAX_SPURIOUS_TURN_END_SKIPS = 3;
|
|
8
12
|
function quoteWin32Arg(arg) {
|
|
9
13
|
if (!/[\s"&|^<>()]/.test(arg))
|
|
10
14
|
return arg;
|
|
@@ -293,6 +297,17 @@ export class ClaudeCodeAdapter {
|
|
|
293
297
|
}
|
|
294
298
|
const groupKey = randomUUID();
|
|
295
299
|
let sawError = false;
|
|
300
|
+
// Poison-frame guard. When `--resume` (with or without `--fork-session`)
|
|
301
|
+
// targets a transcript whose tail is dangling (previous process was
|
|
302
|
+
// killed mid-turn — exactly the fork-on-busy case), Claude CLI emits a
|
|
303
|
+
// spurious EMPTY result frame (num_turns: 0) at startup, BEFORE the
|
|
304
|
+
// queued stdin user frame is processed. Treating its turn_end as the
|
|
305
|
+
// dispatch boundary returns with zero output, the work item sweeps as
|
|
306
|
+
// no_action, and the real reply is orphaned in a soon-to-be-killed
|
|
307
|
+
// subprocess. A legitimate turn always reports num_turns >= 1 (even with
|
|
308
|
+
// no visible text); undefined (CLIs that omit the field) must NOT skip.
|
|
309
|
+
let sawSubstantiveEvent = false;
|
|
310
|
+
let spuriousTurnEndSkips = 0;
|
|
296
311
|
try {
|
|
297
312
|
this.writeUserMessage(state.handle, promptBody);
|
|
298
313
|
}
|
|
@@ -306,6 +321,15 @@ export class ClaudeCodeAdapter {
|
|
|
306
321
|
state.steerReadPending = undefined;
|
|
307
322
|
if (!orphaned.done) {
|
|
308
323
|
const parsed = orphaned.value;
|
|
324
|
+
// An orphaned read can deliver this turn's FIRST event — if it is
|
|
325
|
+
// substantive it must count toward the poison-frame guard below, or
|
|
326
|
+
// a later num_turns=0 turn_end would be wrongly skipped despite real
|
|
327
|
+
// output having been produced this turn.
|
|
328
|
+
if (parsed.type !== 'session_id' &&
|
|
329
|
+
parsed.type !== 'runtime_session' &&
|
|
330
|
+
parsed.type !== 'turn_end') {
|
|
331
|
+
sawSubstantiveEvent = true;
|
|
332
|
+
}
|
|
309
333
|
if (parsed.type === 'session_id') {
|
|
310
334
|
this.opts.sessionManager.recordSessionId(sessionKey, parsed.sessionId);
|
|
311
335
|
yield {
|
|
@@ -354,6 +378,11 @@ export class ClaudeCodeAdapter {
|
|
|
354
378
|
return;
|
|
355
379
|
}
|
|
356
380
|
const parsed = next.value;
|
|
381
|
+
if (parsed.type !== 'session_id' &&
|
|
382
|
+
parsed.type !== 'runtime_session' &&
|
|
383
|
+
parsed.type !== 'turn_end') {
|
|
384
|
+
sawSubstantiveEvent = true;
|
|
385
|
+
}
|
|
357
386
|
if (parsed.type === 'session_id') {
|
|
358
387
|
this.opts.sessionManager.recordSessionId(sessionKey, parsed.sessionId);
|
|
359
388
|
yield {
|
|
@@ -364,6 +393,13 @@ export class ClaudeCodeAdapter {
|
|
|
364
393
|
continue;
|
|
365
394
|
}
|
|
366
395
|
if (parsed.type === 'turn_end') {
|
|
396
|
+
if (parsed.numTurns === 0 &&
|
|
397
|
+
!sawSubstantiveEvent &&
|
|
398
|
+
spuriousTurnEndSkips < MAX_SPURIOUS_TURN_END_SKIPS) {
|
|
399
|
+
spuriousTurnEndSkips++;
|
|
400
|
+
log?.warn?.('ignoring spurious empty turn_end (num_turns=0) — resume/fork poison frame');
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
367
403
|
if (state.needsRestart) {
|
|
368
404
|
this.killProcess(sessionKey, state);
|
|
369
405
|
}
|
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 { ParallAgentGateway, capabilityBinDir, createPlatformConfigManager, createLogger, createOtelLogger, childLogger, deriveModelIsPin, materializeChannelCapabilities, resolveRuntimeModel, parseShutdownDeadlineMs, parseForkDeadlineMs, parseDispatchDeadlineMs, parseProviderConfig, clearAllProviderCreds, llmSource, initAgentTelemetry, } from '@parall/agent-core';
|
|
3
|
+
import { ParallAgentGateway, capabilityBinDir, configureHttpKeepAlive, createPlatformConfigManager, createLogger, createOtelLogger, childLogger, deriveModelIsPin, materializeChannelCapabilities, resolveRuntimeModel, parseShutdownDeadlineMs, parseForkDeadlineMs, parseDispatchDeadlineMs, parseProviderConfig, clearAllProviderCreds, llmSource, initAgentTelemetry, } 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';
|
|
@@ -41,6 +41,8 @@ function resolveProviderEnv() {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
async function main() {
|
|
44
|
+
// Before any fetch: long-lived HTTP connections for every bridge→api call.
|
|
45
|
+
configureHttpKeepAlive();
|
|
44
46
|
const telemetry = await initAgentTelemetry('parall-claude-agent', 'claude-code');
|
|
45
47
|
activeLog = createOtelLogger('agent', 'claude-agent');
|
|
46
48
|
try {
|
package/dist/output-parser.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type ClaudeParsedEvent = RuntimeEvent | {
|
|
|
7
7
|
type: 'turn_end';
|
|
8
8
|
sessionId?: string;
|
|
9
9
|
isError: boolean;
|
|
10
|
+
numTurns?: number;
|
|
10
11
|
};
|
|
11
12
|
export declare function parseClaudeStreamJson(readable: Readable): AsyncGenerator<ClaudeParsedEvent>;
|
|
12
13
|
//# sourceMappingURL=output-parser.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output-parser.d.ts","sourceRoot":"","sources":["../src/output-parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,MAAM,iBAAiB,GACzB,YAAY,GACZ;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"output-parser.d.ts","sourceRoot":"","sources":["../src/output-parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,MAAM,iBAAiB,GACzB,YAAY,GACZ;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAkBzC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAgFlF,wBAAuB,qBAAqB,CAC1C,QAAQ,EAAE,QAAQ,GACjB,cAAc,CAAC,iBAAiB,CAAC,CAmInC"}
|
package/dist/output-parser.js
CHANGED
|
@@ -4,6 +4,9 @@ function asTrimmedString(value) {
|
|
|
4
4
|
const trimmed = value.trim();
|
|
5
5
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
6
6
|
}
|
|
7
|
+
function asFiniteNumber(value) {
|
|
8
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
9
|
+
}
|
|
7
10
|
function parseEventTimestampMs(event) {
|
|
8
11
|
if (!event || typeof event !== 'object')
|
|
9
12
|
return undefined;
|
|
@@ -187,10 +190,12 @@ export async function* parseClaudeStreamJson(readable) {
|
|
|
187
190
|
// the next turn's accounting. Drop them here rather than letting
|
|
188
191
|
// the map grow unbounded or mislabel durations on id collisions.
|
|
189
192
|
toolUses.clear();
|
|
193
|
+
const numTurns = asFiniteNumber(eventRecord.num_turns);
|
|
190
194
|
yield {
|
|
191
195
|
type: 'turn_end',
|
|
192
196
|
sessionId: asTrimmedString(eventRecord.session_id),
|
|
193
197
|
isError,
|
|
198
|
+
...(numTurns !== undefined ? { numTurns } : {}),
|
|
194
199
|
};
|
|
195
200
|
}
|
|
196
201
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/claude-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.47.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.47.0",
|
|
29
|
+
"@parall/cli": "1.47.0",
|
|
30
|
+
"@parall/sdk": "1.47.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^22.0.0",
|
package/src/dispatch.ts
CHANGED
|
@@ -59,6 +59,11 @@ type ClaudeCodeAdapterOptions = Pick<
|
|
|
59
59
|
|
|
60
60
|
const IS_WIN32 = process.platform === 'win32';
|
|
61
61
|
|
|
62
|
+
// Upper bound on spurious empty turn_end frames skipped per turn (see the
|
|
63
|
+
// guard in runTurn) — keeps a pathological stream of empty `result` frames
|
|
64
|
+
// from spinning the drain loop forever.
|
|
65
|
+
const MAX_SPURIOUS_TURN_END_SKIPS = 3;
|
|
66
|
+
|
|
62
67
|
function quoteWin32Arg(arg: string): string {
|
|
63
68
|
if (!/[\s"&|^<>()]/.test(arg)) return arg;
|
|
64
69
|
return `"${arg.replace(/"/g, '""')}"`;
|
|
@@ -399,6 +404,17 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
399
404
|
}
|
|
400
405
|
const groupKey = randomUUID();
|
|
401
406
|
let sawError = false;
|
|
407
|
+
// Poison-frame guard. When `--resume` (with or without `--fork-session`)
|
|
408
|
+
// targets a transcript whose tail is dangling (previous process was
|
|
409
|
+
// killed mid-turn — exactly the fork-on-busy case), Claude CLI emits a
|
|
410
|
+
// spurious EMPTY result frame (num_turns: 0) at startup, BEFORE the
|
|
411
|
+
// queued stdin user frame is processed. Treating its turn_end as the
|
|
412
|
+
// dispatch boundary returns with zero output, the work item sweeps as
|
|
413
|
+
// no_action, and the real reply is orphaned in a soon-to-be-killed
|
|
414
|
+
// subprocess. A legitimate turn always reports num_turns >= 1 (even with
|
|
415
|
+
// no visible text); undefined (CLIs that omit the field) must NOT skip.
|
|
416
|
+
let sawSubstantiveEvent = false;
|
|
417
|
+
let spuriousTurnEndSkips = 0;
|
|
402
418
|
|
|
403
419
|
try {
|
|
404
420
|
this.writeUserMessage(state.handle, promptBody);
|
|
@@ -413,6 +429,17 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
413
429
|
state.steerReadPending = undefined;
|
|
414
430
|
if (!orphaned.done) {
|
|
415
431
|
const parsed = orphaned.value;
|
|
432
|
+
// An orphaned read can deliver this turn's FIRST event — if it is
|
|
433
|
+
// substantive it must count toward the poison-frame guard below, or
|
|
434
|
+
// a later num_turns=0 turn_end would be wrongly skipped despite real
|
|
435
|
+
// output having been produced this turn.
|
|
436
|
+
if (
|
|
437
|
+
parsed.type !== 'session_id' &&
|
|
438
|
+
parsed.type !== 'runtime_session' &&
|
|
439
|
+
parsed.type !== 'turn_end'
|
|
440
|
+
) {
|
|
441
|
+
sawSubstantiveEvent = true;
|
|
442
|
+
}
|
|
416
443
|
if (parsed.type === 'session_id') {
|
|
417
444
|
this.opts.sessionManager.recordSessionId(sessionKey, parsed.sessionId);
|
|
418
445
|
yield {
|
|
@@ -462,6 +489,14 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
462
489
|
|
|
463
490
|
const parsed = next.value;
|
|
464
491
|
|
|
492
|
+
if (
|
|
493
|
+
parsed.type !== 'session_id' &&
|
|
494
|
+
parsed.type !== 'runtime_session' &&
|
|
495
|
+
parsed.type !== 'turn_end'
|
|
496
|
+
) {
|
|
497
|
+
sawSubstantiveEvent = true;
|
|
498
|
+
}
|
|
499
|
+
|
|
465
500
|
if (parsed.type === 'session_id') {
|
|
466
501
|
this.opts.sessionManager.recordSessionId(sessionKey, parsed.sessionId);
|
|
467
502
|
yield {
|
|
@@ -473,6 +508,15 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
473
508
|
}
|
|
474
509
|
|
|
475
510
|
if (parsed.type === 'turn_end') {
|
|
511
|
+
if (
|
|
512
|
+
parsed.numTurns === 0 &&
|
|
513
|
+
!sawSubstantiveEvent &&
|
|
514
|
+
spuriousTurnEndSkips < MAX_SPURIOUS_TURN_END_SKIPS
|
|
515
|
+
) {
|
|
516
|
+
spuriousTurnEndSkips++;
|
|
517
|
+
log?.warn?.('ignoring spurious empty turn_end (num_turns=0) — resume/fork poison frame');
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
476
520
|
if (state.needsRestart) {
|
|
477
521
|
this.killProcess(sessionKey, state);
|
|
478
522
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as os from 'node:os';
|
|
|
4
4
|
import {
|
|
5
5
|
ParallAgentGateway,
|
|
6
6
|
capabilityBinDir,
|
|
7
|
+
configureHttpKeepAlive,
|
|
7
8
|
createPlatformConfigManager,
|
|
8
9
|
createLogger,
|
|
9
10
|
createOtelLogger,
|
|
@@ -67,6 +68,8 @@ function resolveProviderEnv(): void {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
async function main() {
|
|
71
|
+
// Before any fetch: long-lived HTTP connections for every bridge→api call.
|
|
72
|
+
configureHttpKeepAlive();
|
|
70
73
|
const telemetry = await initAgentTelemetry('parall-claude-agent', 'claude-code');
|
|
71
74
|
activeLog = createOtelLogger('agent', 'claude-agent');
|
|
72
75
|
try {
|
package/src/output-parser.ts
CHANGED
|
@@ -14,7 +14,14 @@ export type ClaudeParsedEvent =
|
|
|
14
14
|
// stream. For a short-lived `--print <prompt>` invocation the final
|
|
15
15
|
// `result` frame arrives right before stdout EOF, so this event is
|
|
16
16
|
// redundant there — safe to ignore.
|
|
17
|
-
|
|
17
|
+
//
|
|
18
|
+
// `numTurns` mirrors the frame's `num_turns` (absent on CLIs that don't
|
|
19
|
+
// report it): a legitimate turn always reports >= 1 even when the model
|
|
20
|
+
// replies with no visible text, while the spurious empty result frame the
|
|
21
|
+
// CLI emits at startup when `--resume` (with or without `--fork-session`)
|
|
22
|
+
// targets a dangling-tail transcript reports 0 — consumers use it to
|
|
23
|
+
// discriminate that poison frame from a real turn boundary.
|
|
24
|
+
| { type: 'turn_end'; sessionId?: string; isError: boolean; numTurns?: number };
|
|
18
25
|
|
|
19
26
|
type ToolUseMeta = {
|
|
20
27
|
toolName: string;
|
|
@@ -27,6 +34,10 @@ function asTrimmedString(value: unknown): string | undefined {
|
|
|
27
34
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
28
35
|
}
|
|
29
36
|
|
|
37
|
+
function asFiniteNumber(value: unknown): number | undefined {
|
|
38
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
function parseEventTimestampMs(event: unknown): number | undefined {
|
|
31
42
|
if (!event || typeof event !== 'object') return undefined;
|
|
32
43
|
const timestamp = (event as { timestamp?: unknown }).timestamp;
|
|
@@ -214,10 +225,12 @@ export async function* parseClaudeStreamJson(
|
|
|
214
225
|
// the next turn's accounting. Drop them here rather than letting
|
|
215
226
|
// the map grow unbounded or mislabel durations on id collisions.
|
|
216
227
|
toolUses.clear();
|
|
228
|
+
const numTurns = asFiniteNumber(eventRecord.num_turns);
|
|
217
229
|
yield {
|
|
218
230
|
type: 'turn_end',
|
|
219
231
|
sessionId: asTrimmedString(eventRecord.session_id),
|
|
220
232
|
isError,
|
|
233
|
+
...(numTurns !== undefined ? { numTurns } : {}),
|
|
221
234
|
};
|
|
222
235
|
}
|
|
223
236
|
}
|