@parall/claude-agent 1.45.0 → 1.46.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.
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
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 {
@@ -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,GAWzC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AA4E/D,wBAAuB,qBAAqB,CAC1C,QAAQ,EAAE,QAAQ,GACjB,cAAc,CAAC,iBAAiB,CAAC,CAiInC"}
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"}
@@ -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.45.0",
3
+ "version": "1.46.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.45.0",
29
- "@parall/cli": "1.45.0",
30
- "@parall/sdk": "1.45.0"
28
+ "@parall/agent-core": "1.46.0",
29
+ "@parall/cli": "1.46.0",
30
+ "@parall/sdk": "1.46.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 {
@@ -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
- | { type: 'turn_end'; sessionId?: string; isError: boolean };
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
  }