@parall/claude-agent 1.63.0 → 1.64.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,4 +1,4 @@
1
- import type { CleanupForkOpts, CompactOpts, CompactResult, DispatchAdapter, DispatchInputLifecycle, DispatchOpts, ForkOpts, GatewayLogger, RuntimeActivityEvent, RuntimeBusyState, RuntimeEvent } from '@parall/agent-core';
1
+ import type { CleanupForkOpts, CompactOpts, CompactResult, DiscardedInjectionOutcome, DispatchAdapter, DispatchInputLifecycle, DispatchOpts, ForkOpts, GatewayLogger, RuntimeActivityEvent, RuntimeBusyState, RuntimeEvent } from '@parall/agent-core';
2
2
  import type { ClaudeAgentConfig } from './config.js';
3
3
  import { ClaudeInputRegistry } from './input-lifecycle.js';
4
4
  import { ClaudeProcessPump } from './process-pump.js';
@@ -60,6 +60,7 @@ export type ProcessState = {
60
60
  export declare class ClaudeCodeAdapter implements DispatchAdapter {
61
61
  private readonly opts;
62
62
  readonly inputLifecycleMode: "explicit";
63
+ readonly inputSettlementMode: "receipt";
63
64
  private readonly processes;
64
65
  /** Retired by abortDispatch while the CLI still had its own work: busy until idle, then terminated. */
65
66
  private readonly retiring;
@@ -88,7 +89,19 @@ export declare class ClaudeCodeAdapter implements DispatchAdapter {
88
89
  abortDispatch(sessionKey: string): void;
89
90
  hasPendingInjections(sessionKey: string): boolean;
90
91
  hasUnsettledInjections(sessionKey: string): boolean;
91
- acknowledgeDiscardedInjection(sessionKey: string, deliveryKey: string): void;
92
+ /**
93
+ * The gateway discarded the buffered copy of an injection without a
94
+ * dispatch(). A settled injection only owes bookkeeping. One that has NOT
95
+ * settled is an orphan: nothing will ever drain it, and the CLI does not
96
+ * start a queued input on its own once the turn it was steered into has
97
+ * ended — it works its queue only when the next stdin frame arrives, which
98
+ * would be the batch re-delivering this very member (prod 2026-09-03: lane
99
+ * dethroned after 10 idle minutes, then "refusing duplicate stdin write"
100
+ * on every retry). Withdraw it: kill the process (the session survives via
101
+ * --resume), report the input failed so the server releases the member,
102
+ * and let the gateway run it as a real turn.
103
+ */
104
+ acknowledgeDiscardedInjection(sessionKey: string, deliveryKey: string): Promise<DiscardedInjectionOutcome>;
92
105
  dispatch({ event, bodyForAgent, sessionKey, context, inputLifecycle, noteActivity, }: DispatchOpts): AsyncIterable<RuntimeEvent>;
93
106
  getBranchPoint(_sessionKey: string): string | undefined;
94
107
  getSessionHistoryPath(sessionKey: string): string | undefined;
@@ -125,6 +138,7 @@ export declare class ClaudeCodeAdapter implements DispatchAdapter {
125
138
  private maybeApplyRestart;
126
139
  private ensureProcess;
127
140
  private spawnProcess;
141
+ /** Resolves once every live input's failed report has settled (best effort). */
128
142
  private killProcess;
129
143
  private endStdin;
130
144
  private terminateHandle;
@@ -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
@@ -33,6 +33,7 @@ function killWin32Tree(pid) {
33
33
  export class ClaudeCodeAdapter {
34
34
  opts;
35
35
  inputLifecycleMode = 'explicit';
36
+ inputSettlementMode = 'receipt';
36
37
  processes = new Map();
37
38
  /** Retired by abortDispatch while the CLI still had its own work: busy until idle, then terminated. */
38
39
  retiring = new Set();
@@ -169,16 +170,44 @@ export class ClaudeCodeAdapter {
169
170
  return false;
170
171
  return state.inputs.hasUnsettledInjections();
171
172
  }
172
- acknowledgeDiscardedInjection(sessionKey, deliveryKey) {
173
+ /**
174
+ * The gateway discarded the buffered copy of an injection without a
175
+ * dispatch(). A settled injection only owes bookkeeping. One that has NOT
176
+ * settled is an orphan: nothing will ever drain it, and the CLI does not
177
+ * start a queued input on its own once the turn it was steered into has
178
+ * ended — it works its queue only when the next stdin frame arrives, which
179
+ * would be the batch re-delivering this very member (prod 2026-09-03: lane
180
+ * dethroned after 10 idle minutes, then "refusing duplicate stdin write"
181
+ * on every retry). Withdraw it: kill the process (the session survives via
182
+ * --resume), report the input failed so the server releases the member,
183
+ * and let the gateway run it as a real turn.
184
+ */
185
+ async acknowledgeDiscardedInjection(sessionKey, deliveryKey) {
173
186
  const state = this.processes.get(sessionKey);
174
187
  if (!state)
175
- return;
188
+ return 'discarded';
189
+ const delivery = state.inputs.getByKey(deliveryKey);
190
+ if (delivery && !delivery.terminal) {
191
+ state.log?.warn?.(`withdrawing orphaned Claude input ${delivery.commandUuid} (${delivery.reportedState ?? 'queued'}) on ${sessionKey}: its buffered copy was discarded before the CLI settled it${state.pump.hasOwnWork() ? '; the runtime-initiated turn dies with the process' : ''}`);
192
+ await this.killProcess(sessionKey, state);
193
+ state.inputs.discardBookkeeping(deliveryKey);
194
+ // 'cancelled' only when the server confirmed the failed receipt: the
195
+ // member is released and un-folded, so the gateway can re-fold and run
196
+ // it locally. A lost receipt (reportedState never reached 'failed') or
197
+ // a 'settled' answer (a reply effect already resolved the member)
198
+ // leaves nothing to re-run here — the lane complete releases an unread
199
+ // member for re-drive instead.
200
+ const confirmed = delivery.reportedState === 'failed' && delivery.terminal === 'failed';
201
+ if (!confirmed) {
202
+ state.log?.warn?.(`withdrawal of Claude input ${delivery.commandUuid} was not confirmed by the server (${delivery.terminal}); leaving the member to the lane complete`);
203
+ }
204
+ return confirmed ? 'cancelled' : 'discarded';
205
+ }
176
206
  state.inputs.discardBookkeeping(deliveryKey);
177
207
  // 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.
208
+ // waiting on.
181
209
  this.maybeApplyRestart(sessionKey, state);
210
+ return 'discarded';
182
211
  }
183
212
  async *dispatch({ event, bodyForAgent, sessionKey, context, inputLifecycle, noteActivity, }) {
184
213
  const deliveryKey = inputLifecycle?.deliveryKey ?? event.dispatchEventId ?? event.messageId;
@@ -246,13 +275,13 @@ export class ClaudeCodeAdapter {
246
275
  cleanupFork({ fork }) {
247
276
  const state = this.processes.get(fork.sessionKey);
248
277
  if (state) {
249
- this.killProcess(fork.sessionKey, state);
278
+ void this.killProcess(fork.sessionKey, state);
250
279
  }
251
280
  this.opts.sessionManager.cleanupFork(fork.sessionKey);
252
281
  }
253
282
  resetProcesses() {
254
283
  for (const [sessionKey, state] of this.processes) {
255
- this.killProcess(sessionKey, state);
284
+ void this.killProcess(sessionKey, state);
256
285
  }
257
286
  this.processes.clear();
258
287
  const retired = [...this.retiring];
@@ -303,7 +332,7 @@ export class ClaudeCodeAdapter {
303
332
  await state.inputs.failBestEffort(delivery, log, `Claude stdin write failed: ${String(err)}`);
304
333
  state.inputs.remove(delivery);
305
334
  yield { type: 'error', message: `Claude stdin write failed: ${String(err)}` };
306
- this.killProcess(sessionKey, state);
335
+ void this.killProcess(sessionKey, state);
307
336
  return;
308
337
  }
309
338
  try {
@@ -318,7 +347,9 @@ export class ClaudeCodeAdapter {
318
347
  return runClaudeCompact({
319
348
  ensureRuntimeCapability: (log) => this.ensureRuntimeCapability(log),
320
349
  ensureProcess: (sessionKey, log) => this.ensureProcess(sessionKey, log),
321
- killProcess: (sessionKey, state) => this.killProcess(sessionKey, state),
350
+ killProcess: (sessionKey, state) => {
351
+ void this.killProcess(sessionKey, state);
352
+ },
322
353
  writeUserMessage: (handle, text, uuid) => this.writeUserMessage(handle, text, uuid),
323
354
  applyPendingRestart: (sessionKey, state) => this.maybeApplyRestart(sessionKey, state),
324
355
  }, opts);
@@ -451,7 +482,7 @@ export class ClaudeCodeAdapter {
451
482
  if (outstanding.total > 0) {
452
483
  state.log?.warn?.(`restarting ${sessionKey} with ${outstanding.total} live background task(s); they die with the process`);
453
484
  }
454
- this.killProcess(sessionKey, state);
485
+ void this.killProcess(sessionKey, state);
455
486
  }
456
487
  ensureProcess(sessionKey, log) {
457
488
  if (this.shuttingDown) {
@@ -466,7 +497,7 @@ export class ClaudeCodeAdapter {
466
497
  log?.info?.(`deferring lazy restart of ${sessionKey}: runtime-initiated turn in progress`);
467
498
  return existing;
468
499
  }
469
- this.killProcess(sessionKey, existing);
500
+ void this.killProcess(sessionKey, existing);
470
501
  }
471
502
  else if (alive) {
472
503
  return existing;
@@ -501,7 +532,7 @@ export class ClaudeCodeAdapter {
501
532
  onFatal: () => {
502
533
  const current = this.processes.get(sessionKey);
503
534
  if (current === state)
504
- this.killProcess(sessionKey, state);
535
+ void this.killProcess(sessionKey, state);
505
536
  },
506
537
  onEof: () => {
507
538
  state.done = true;
@@ -564,10 +595,12 @@ export class ClaudeCodeAdapter {
564
595
  });
565
596
  return { proc, exitPromise, stderrChunks };
566
597
  }
598
+ /** Resolves once every live input's failed report has settled (best effort). */
567
599
  killProcess(sessionKey, state) {
600
+ const reports = [];
568
601
  for (const delivery of state.inputs.values()) {
569
602
  if (!delivery.terminal)
570
- void state.inputs.failBestEffort(delivery);
603
+ reports.push(state.inputs.failBestEffort(delivery, state.log));
571
604
  }
572
605
  state.done = true;
573
606
  const current = this.processes.get(sessionKey);
@@ -576,6 +609,7 @@ export class ClaudeCodeAdapter {
576
609
  }
577
610
  state.pump.close('killed', 'Claude process terminated by the bridge');
578
611
  this.terminateHandle(state.handle);
612
+ return Promise.all(reports).then(() => undefined);
579
613
  }
580
614
  endStdin(handle) {
581
615
  try {
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, resolveServiceVersion, materializeChannelCapabilities, ParallAgentGateway, parseDispatchDeadlineMs, parseForkDeadlineMs, parseProviderConfig, parseShutdownDeadlineMs, resolveRuntimeContextWindow, resolveRuntimeMaxTokens, resolveRuntimeModel, traceContextHeaders, } from '@parall/agent-core';
3
+ import { classifyStartupFailure, capabilityBinDir, childLogger, clearAllProviderCreds, configureHttpKeepAlive, createLogger, createOtelLogger, createPlatformConfigManager, deriveModelIsPin, identityFromMe, initAgentTelemetry, llmSource, resolveServiceVersion, materializeChannelCapabilities, ParallAgentGateway, parseDispatchDeadlineMs, parseForkDeadlineMs, parseProviderConfig, parseShutdownDeadlineMs, resolveRuntimeContextWindow, resolveRuntimeMaxTokens, resolveRuntimeModel, traceContextHeaders, newRuntimeInstanceId, } 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';
@@ -84,8 +84,12 @@ async function main() {
84
84
  const sessionStateFilePath = sessionStateFilePathForRuntime(config.stateDir, runtimeKey);
85
85
  const sessionManager = new ClaudeSessionManager(runtimeKey, sessionStateFilePath, agentLog);
86
86
  const resolvedWsUrl = resolveWsUrl(config.apiUrl, config.wsUrl, config.swimlaneName);
87
+ // This process's runtime instance: rides the WS ticket and every claim so
88
+ // the server owns turns by process identity instead of a lease clock
89
+ // (agent-dispatch-idempotency-design.md#instance-ownership).
90
+ const instanceId = newRuntimeInstanceId();
87
91
  const ws = new ParallWs({
88
- getTicket: () => client.getWsTicket(),
92
+ getTicket: () => client.getWsTicket({ instance_id: instanceId }),
89
93
  wsUrl: resolvedWsUrl,
90
94
  });
91
95
  const configMgr = createPlatformConfigManager({
@@ -237,6 +241,7 @@ async function main() {
237
241
  accountId: agentUserId,
238
242
  client,
239
243
  ws,
244
+ instanceId,
240
245
  connectionLabel: resolvedWsUrl,
241
246
  config: {
242
247
  parall_url: config.apiUrl,
@@ -300,6 +305,19 @@ async function main() {
300
305
  }
301
306
  }
302
307
  main().catch((err) => {
303
- activeLog.error(`fatal: ${String(err)}`);
304
- process.exitCode = 1;
308
+ // A credential the server rejects during bootstrap is the same terminal
309
+ // failure the gateway reports as auth-invalid: exit with the re-mint code
310
+ // so the daemon mints a fresh launch credential instead of restarting
311
+ // this process with the same dead key.
312
+ const { exitCode, authRejection } = classifyStartupFailure(err);
313
+ if (authRejection) {
314
+ activeLog.error(`AUTH-INVALID at startup status=${authRejection.status}` +
315
+ `${authRejection.code ? ` code=${authRejection.code}` : ''} — ` +
316
+ `the server rejected this runtime's credential (${authRejection.message}); ` +
317
+ `exiting with code ${exitCode} so the supervisor can re-mint the launch credential.`);
318
+ }
319
+ else {
320
+ activeLog.error(`fatal: ${String(err)}`);
321
+ }
322
+ process.exitCode = exitCode;
305
323
  });
@@ -1 +1 @@
1
- {"version":3,"file":"process-pump.d.ts","sourceRoot":"","sources":["../src/process-pump.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"process-pump.d.ts","sourceRoot":"","sources":["../src/process-pump.ts"],"names":[],"mappings":"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"}
@@ -321,6 +321,9 @@ export class ClaudeProcessPump {
321
321
  kind: 'runtime',
322
322
  event: { type: 'error', message: `Claude input lifecycle update failed: ${String(err)}` },
323
323
  });
324
+ // failBestEffort may unregister this delivery before onFatal closes
325
+ // the process's remaining sinks. Its waiting drain still needs EOF.
326
+ delivery.sink.close();
324
327
  this.opts.hooks.onFatal();
325
328
  return;
326
329
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/claude-agent",
3
- "version": "1.63.0",
3
+ "version": "1.64.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.63.0",
29
- "@parall/cli": "1.63.0",
30
- "@parall/sdk": "1.63.0"
28
+ "@parall/agent-core": "1.64.0",
29
+ "@parall/cli": "1.64.0",
30
+ "@parall/sdk": "1.64.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^22.0.0",
package/src/dispatch.ts CHANGED
@@ -6,6 +6,7 @@ import type {
6
6
  CleanupForkOpts,
7
7
  CompactOpts,
8
8
  CompactResult,
9
+ DiscardedInjectionOutcome,
9
10
  DispatchAdapter,
10
11
  DispatchInputLifecycle,
11
12
  DispatchOpts,
@@ -119,6 +120,7 @@ export type ProcessState = {
119
120
 
120
121
  export class ClaudeCodeAdapter implements DispatchAdapter {
121
122
  readonly inputLifecycleMode = 'explicit' as const;
123
+ readonly inputSettlementMode = 'receipt' as const;
122
124
  private readonly processes = new Map<string, ProcessState>();
123
125
  /** Retired by abortDispatch while the CLI still had its own work: busy until idle, then terminated. */
124
126
  private readonly retiring = new Set<ProcessState>();
@@ -260,15 +262,50 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
260
262
  return state.inputs.hasUnsettledInjections();
261
263
  }
262
264
 
263
- acknowledgeDiscardedInjection(sessionKey: string, deliveryKey: string): void {
265
+ /**
266
+ * The gateway discarded the buffered copy of an injection without a
267
+ * dispatch(). A settled injection only owes bookkeeping. One that has NOT
268
+ * settled is an orphan: nothing will ever drain it, and the CLI does not
269
+ * start a queued input on its own once the turn it was steered into has
270
+ * ended — it works its queue only when the next stdin frame arrives, which
271
+ * would be the batch re-delivering this very member (prod 2026-09-03: lane
272
+ * dethroned after 10 idle minutes, then "refusing duplicate stdin write"
273
+ * on every retry). Withdraw it: kill the process (the session survives via
274
+ * --resume), report the input failed so the server releases the member,
275
+ * and let the gateway run it as a real turn.
276
+ */
277
+ async acknowledgeDiscardedInjection(
278
+ sessionKey: string,
279
+ deliveryKey: string,
280
+ ): Promise<DiscardedInjectionOutcome> {
264
281
  const state = this.processes.get(sessionKey);
265
- if (!state) return;
282
+ if (!state) return 'discarded';
283
+ const delivery = state.inputs.getByKey(deliveryKey);
284
+ if (delivery && !delivery.terminal) {
285
+ state.log?.warn?.(
286
+ `withdrawing orphaned Claude input ${delivery.commandUuid} (${delivery.reportedState ?? 'queued'}) on ${sessionKey}: its buffered copy was discarded before the CLI settled it${state.pump.hasOwnWork() ? '; the runtime-initiated turn dies with the process' : ''}`,
287
+ );
288
+ await this.killProcess(sessionKey, state);
289
+ state.inputs.discardBookkeeping(deliveryKey);
290
+ // 'cancelled' only when the server confirmed the failed receipt: the
291
+ // member is released and un-folded, so the gateway can re-fold and run
292
+ // it locally. A lost receipt (reportedState never reached 'failed') or
293
+ // a 'settled' answer (a reply effect already resolved the member)
294
+ // leaves nothing to re-run here — the lane complete releases an unread
295
+ // member for re-drive instead.
296
+ const confirmed = delivery.reportedState === 'failed' && delivery.terminal === 'failed';
297
+ if (!confirmed) {
298
+ state.log?.warn?.(
299
+ `withdrawal of Claude input ${delivery.commandUuid} was not confirmed by the server (${delivery.terminal}); leaving the member to the lane complete`,
300
+ );
301
+ }
302
+ return confirmed ? 'cancelled' : 'discarded';
303
+ }
266
304
  state.inputs.discardBookkeeping(deliveryKey);
267
305
  // The discarded copy may have been the last thing a lazy restart was
268
- // waiting on. Unlike the consume path's tail, the discarded delivery may
269
- // still be RUNNING (non-terminal): killing the process now would fail a
270
- // live input, so the restart also waits for every injection to settle.
306
+ // waiting on.
271
307
  this.maybeApplyRestart(sessionKey, state);
308
+ return 'discarded';
272
309
  }
273
310
 
274
311
  async *dispatch({
@@ -360,14 +397,14 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
360
397
  cleanupFork({ fork }: CleanupForkOpts) {
361
398
  const state = this.processes.get(fork.sessionKey);
362
399
  if (state) {
363
- this.killProcess(fork.sessionKey, state);
400
+ void this.killProcess(fork.sessionKey, state);
364
401
  }
365
402
  this.opts.sessionManager.cleanupFork(fork.sessionKey);
366
403
  }
367
404
 
368
405
  resetProcesses(): void {
369
406
  for (const [sessionKey, state] of this.processes) {
370
- this.killProcess(sessionKey, state);
407
+ void this.killProcess(sessionKey, state);
371
408
  }
372
409
  this.processes.clear();
373
410
  const retired = [...this.retiring];
@@ -425,7 +462,7 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
425
462
  await state.inputs.failBestEffort(delivery, log, `Claude stdin write failed: ${String(err)}`);
426
463
  state.inputs.remove(delivery);
427
464
  yield { type: 'error', message: `Claude stdin write failed: ${String(err)}` };
428
- this.killProcess(sessionKey, state);
465
+ void this.killProcess(sessionKey, state);
429
466
  return;
430
467
  }
431
468
 
@@ -442,7 +479,9 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
442
479
  {
443
480
  ensureRuntimeCapability: (log) => this.ensureRuntimeCapability(log),
444
481
  ensureProcess: (sessionKey, log) => this.ensureProcess(sessionKey, log),
445
- killProcess: (sessionKey, state) => this.killProcess(sessionKey, state),
482
+ killProcess: (sessionKey, state) => {
483
+ void this.killProcess(sessionKey, state);
484
+ },
446
485
  writeUserMessage: (handle, text, uuid) => this.writeUserMessage(handle, text, uuid),
447
486
  applyPendingRestart: (sessionKey, state) => this.maybeApplyRestart(sessionKey, state),
448
487
  },
@@ -583,7 +622,7 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
583
622
  `restarting ${sessionKey} with ${outstanding.total} live background task(s); they die with the process`,
584
623
  );
585
624
  }
586
- this.killProcess(sessionKey, state);
625
+ void this.killProcess(sessionKey, state);
587
626
  }
588
627
 
589
628
  private ensureProcess(sessionKey: string, log: GatewayLogger | undefined): ProcessState {
@@ -602,7 +641,7 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
602
641
  );
603
642
  return existing;
604
643
  }
605
- this.killProcess(sessionKey, existing);
644
+ void this.killProcess(sessionKey, existing);
606
645
  } else if (alive) {
607
646
  return existing;
608
647
  } else {
@@ -635,7 +674,7 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
635
674
  },
636
675
  onFatal: () => {
637
676
  const current = this.processes.get(sessionKey);
638
- if (current === state) this.killProcess(sessionKey, state);
677
+ if (current === state) void this.killProcess(sessionKey, state);
639
678
  },
640
679
  onEof: () => {
641
680
  state.done = true;
@@ -723,9 +762,11 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
723
762
  return { proc, exitPromise, stderrChunks };
724
763
  }
725
764
 
726
- private killProcess(sessionKey: string, state: ProcessState) {
765
+ /** Resolves once every live input's failed report has settled (best effort). */
766
+ private killProcess(sessionKey: string, state: ProcessState): Promise<void> {
767
+ const reports: Promise<void>[] = [];
727
768
  for (const delivery of state.inputs.values()) {
728
- if (!delivery.terminal) void state.inputs.failBestEffort(delivery);
769
+ if (!delivery.terminal) reports.push(state.inputs.failBestEffort(delivery, state.log));
729
770
  }
730
771
  state.done = true;
731
772
  const current = this.processes.get(sessionKey);
@@ -734,6 +775,7 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
734
775
  }
735
776
  state.pump.close('killed', 'Claude process terminated by the bridge');
736
777
  this.terminateHandle(state.handle);
778
+ return Promise.all(reports).then(() => undefined);
737
779
  }
738
780
 
739
781
  private endStdin(handle: ClaudeProcessHandle): void {
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as os from 'node:os';
4
4
  import {
5
+ classifyStartupFailure,
5
6
  capabilityBinDir,
6
7
  childLogger,
7
8
  clearAllProviderCreds,
@@ -24,6 +25,7 @@ import {
24
25
  resolveRuntimeMaxTokens,
25
26
  resolveRuntimeModel,
26
27
  traceContextHeaders,
28
+ newRuntimeInstanceId,
27
29
  } from '@parall/agent-core';
28
30
  import { ApiError, ParallClient, ParallWs } from '@parall/sdk';
29
31
  import {
@@ -124,8 +126,12 @@ async function main() {
124
126
  const sessionManager = new ClaudeSessionManager(runtimeKey, sessionStateFilePath, agentLog);
125
127
 
126
128
  const resolvedWsUrl = resolveWsUrl(config.apiUrl, config.wsUrl, config.swimlaneName);
129
+ // This process's runtime instance: rides the WS ticket and every claim so
130
+ // the server owns turns by process identity instead of a lease clock
131
+ // (agent-dispatch-idempotency-design.md#instance-ownership).
132
+ const instanceId = newRuntimeInstanceId();
127
133
  const ws = new ParallWs({
128
- getTicket: () => client.getWsTicket(),
134
+ getTicket: () => client.getWsTicket({ instance_id: instanceId }),
129
135
  wsUrl: resolvedWsUrl,
130
136
  });
131
137
 
@@ -299,6 +305,7 @@ async function main() {
299
305
  accountId: agentUserId,
300
306
  client,
301
307
  ws,
308
+ instanceId,
302
309
  connectionLabel: resolvedWsUrl,
303
310
  config: {
304
311
  parall_url: config.apiUrl,
@@ -365,6 +372,20 @@ async function main() {
365
372
  }
366
373
 
367
374
  main().catch((err) => {
368
- activeLog.error(`fatal: ${String(err)}`);
369
- process.exitCode = 1;
375
+ // A credential the server rejects during bootstrap is the same terminal
376
+ // failure the gateway reports as auth-invalid: exit with the re-mint code
377
+ // so the daemon mints a fresh launch credential instead of restarting
378
+ // this process with the same dead key.
379
+ const { exitCode, authRejection } = classifyStartupFailure(err);
380
+ if (authRejection) {
381
+ activeLog.error(
382
+ `AUTH-INVALID at startup status=${authRejection.status}` +
383
+ `${authRejection.code ? ` code=${authRejection.code}` : ''} — ` +
384
+ `the server rejected this runtime's credential (${authRejection.message}); ` +
385
+ `exiting with code ${exitCode} so the supervisor can re-mint the launch credential.`,
386
+ );
387
+ } else {
388
+ activeLog.error(`fatal: ${String(err)}`);
389
+ }
390
+ process.exitCode = exitCode;
370
391
  });
@@ -361,6 +361,9 @@ export class ClaudeProcessPump {
361
361
  kind: 'runtime',
362
362
  event: { type: 'error', message: `Claude input lifecycle update failed: ${String(err)}` },
363
363
  });
364
+ // failBestEffort may unregister this delivery before onFatal closes
365
+ // the process's remaining sinks. Its waiting drain still needs EOF.
366
+ delivery.sink.close();
364
367
  this.opts.hooks.onFatal();
365
368
  return;
366
369
  }