@parall/claude-agent 1.63.0 → 1.65.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;
@@ -509,10 +540,9 @@ export class ClaudeCodeAdapter {
509
540
  this.processes.delete(sessionKey);
510
541
  this.retiring.delete(state);
511
542
  },
512
- // Main-session turns go to the gateway (steps + session activity);
513
- // fork-session turns only count as busy (fork-scope invariant).
514
543
  onRuntimeTurnOpened: (turn) => this.activity.surfaceTurn(turn, this.opts.sessionManager.isMain(sessionKey), log),
515
544
  onRuntimeTurnClosed: () => this.maybeApplyRestart(sessionKey, state),
545
+ onObservation: (event) => this.activity.emit({ kind: 'observation', sessionKey, event }, log),
516
546
  },
517
547
  }),
518
548
  done: false,
@@ -564,10 +594,12 @@ export class ClaudeCodeAdapter {
564
594
  });
565
595
  return { proc, exitPromise, stderrChunks };
566
596
  }
597
+ /** Resolves once every live input's failed report has settled (best effort). */
567
598
  killProcess(sessionKey, state) {
599
+ const reports = [];
568
600
  for (const delivery of state.inputs.values()) {
569
601
  if (!delivery.terminal)
570
- void state.inputs.failBestEffort(delivery);
602
+ reports.push(state.inputs.failBestEffort(delivery, state.log));
571
603
  }
572
604
  state.done = true;
573
605
  const current = this.processes.get(sessionKey);
@@ -576,6 +608,7 @@ export class ClaudeCodeAdapter {
576
608
  }
577
609
  state.pump.close('killed', 'Claude process terminated by the bridge');
578
610
  this.terminateHandle(state.handle);
611
+ return Promise.all(reports).then(() => undefined);
579
612
  }
580
613
  endStdin(handle) {
581
614
  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":"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;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EACH,0BAA0B,GAC1B,cAAc,GACd,eAAe,GACf,cAAc,GACd,mBAAmB,GACnB,QAAQ,GACR,uBAAuB,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC/F,CAAC;AAYF,MAAM,MAAM,iBAAiB,GACzB,YAAY,GACZ;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,GACpE,sBAAsB,GAItB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAKnC;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,GAC5B;IACE,IAAI,EAAE,mBAAmB,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;CACvE,GAID;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAI5C;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAqBlE;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B,CAAC;AAgFN,wBAAuB,qBAAqB,CAC1C,QAAQ,EAAE,QAAQ,GACjB,cAAc,CAAC,iBAAiB,CAAC,CA6NnC"}
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;AAGvD;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EACH,0BAA0B,GAC1B,cAAc,GACd,eAAe,GACf,cAAc,GACd,mBAAmB,GACnB,QAAQ,GACR,uBAAuB,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC/F,CAAC;AAYF,MAAM,MAAM,iBAAiB,GACzB,YAAY,GACZ;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,GACpE,sBAAsB,GAItB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAKnC;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,GAC5B;IACE,IAAI,EAAE,mBAAmB,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;CACvE,GAID;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAI5C;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAqBlE;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B,CAAC;AAgFN,wBAAuB,qBAAqB,CAC1C,QAAQ,EAAE,QAAQ,GACjB,cAAc,CAAC,iBAAiB,CAAC,CAiOnC"}
@@ -1,3 +1,4 @@
1
+ import { claudeObservation } from './runtime-observations.js';
1
2
  const RUNTIME_TASK_SUBTYPES = new Set([
2
3
  'background_tasks_changed',
3
4
  'task_started',
@@ -100,6 +101,10 @@ export async function* parseClaudeStreamJson(readable) {
100
101
  const now = Date.now();
101
102
  const eventTimestampMs = parseEventTimestampMs(event) ?? now;
102
103
  const eventRecord = event;
104
+ const observation = claudeObservation(eventRecord);
105
+ // Init must announce the native session before its observations drain.
106
+ if (observation && observation.content.event !== 'session_initialized')
107
+ yield observation;
103
108
  if (eventRecord.type === 'system' && eventRecord.subtype === 'compact_boundary') {
104
109
  const meta = eventRecord.compact_metadata && typeof eventRecord.compact_metadata === 'object'
105
110
  ? eventRecord.compact_metadata
@@ -126,6 +131,8 @@ export async function* parseClaudeStreamJson(readable) {
126
131
  ...(sessionId ? { sessionId } : {}),
127
132
  capabilities,
128
133
  };
134
+ if (observation)
135
+ yield observation;
129
136
  continue;
130
137
  }
131
138
  const subtype = asTrimmedString(eventRecord.subtype);
@@ -1,4 +1,4 @@
1
- import { type GatewayLogger, type RuntimeBusyState } from '@parall/agent-core';
1
+ import { type GatewayLogger, type RuntimeBusyState, type RuntimeObservationEvent } from '@parall/agent-core';
2
2
  import { ClaudeBusyTracker } from './busy-state.js';
3
3
  import type { ClaudeInputDelivery, ClaudeInputRegistry } from './input-lifecycle.js';
4
4
  import type { ClaudeParsedEvent } from './output-parser.js';
@@ -17,6 +17,7 @@ export type ClaudeProcessPumpHooks = {
17
17
  onEof(): void;
18
18
  onRuntimeTurnOpened(turn: ClaudeRuntimeTurn): void;
19
19
  onRuntimeTurnClosed(): void;
20
+ onObservation?(event: RuntimeObservationEvent): void;
20
21
  };
21
22
  export type ClaudeProcessPumpOptions = {
22
23
  sessionKey: string;
@@ -59,6 +60,7 @@ export declare class ClaudeProcessPump {
59
60
  private idleTimer;
60
61
  private activeDrain;
61
62
  private readonly now;
63
+ private pendingObservations;
62
64
  constructor(opts: ClaudeProcessPumpOptions);
63
65
  start(): void;
64
66
  /** The dispatch generator currently draining `delivery` (owner preference). */
@@ -92,6 +94,7 @@ export declare class ClaudeProcessPump {
92
94
  private run;
93
95
  private route;
94
96
  private touchAll;
97
+ private routeObservation;
95
98
  private liveDeliveries;
96
99
  private ownerFor;
97
100
  private openRuntimeTurn;
@@ -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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"}
@@ -33,6 +33,7 @@ export class ClaudeProcessPump {
33
33
  idleTimer = null;
34
34
  activeDrain = null;
35
35
  now;
36
+ pendingObservations = [];
36
37
  constructor(opts) {
37
38
  this.opts = opts;
38
39
  this.now = opts.now ?? Date.now;
@@ -153,7 +154,8 @@ export class ClaudeProcessPump {
153
154
  for await (const parsed of parser) {
154
155
  if (this.closed)
155
156
  break;
156
- this.touchAll();
157
+ if (parsed.type !== 'observation')
158
+ this.touchAll();
157
159
  await this.route(parsed);
158
160
  }
159
161
  }
@@ -168,6 +170,9 @@ export class ClaudeProcessPump {
168
170
  switch (parsed.type) {
169
171
  case 'runtime_activity':
170
172
  return;
173
+ case 'observation':
174
+ this.routeObservation(parsed);
175
+ return;
171
176
  case 'runtime_init':
172
177
  this.handleInit(parsed);
173
178
  return;
@@ -222,6 +227,27 @@ export class ClaudeProcessPump {
222
227
  }
223
228
  this.runtimeTurn?.touch();
224
229
  }
230
+ routeObservation(event) {
231
+ if (!this.sessionId) {
232
+ // SessionStart hooks precede init. Keep a bounded startup buffer and
233
+ // release only records whose native session matches the eventual init.
234
+ if (this.pendingObservations.length < 64)
235
+ this.pendingObservations.push(event);
236
+ else
237
+ this.opts.log?.warn('Claude startup observation buffer full; dropping observation');
238
+ return;
239
+ }
240
+ if (event.runtimeSessionId !== this.sessionId) {
241
+ this.opts.log?.warn('Claude observation does not match the initialized session');
242
+ return;
243
+ }
244
+ const owner = this.ownerFor(false) ??
245
+ (this.activeDrain && !this.activeDrain.terminal ? this.activeDrain : null);
246
+ if (owner)
247
+ owner.sink.push({ kind: 'runtime', event });
248
+ else
249
+ this.opts.hooks.onObservation?.(event);
250
+ }
225
251
  liveDeliveries() {
226
252
  const live = [];
227
253
  for (const delivery of this.opts.inputs.values())
@@ -293,6 +319,10 @@ export class ClaudeProcessPump {
293
319
  delivery.sessionAnnounced = true;
294
320
  delivery.sink.push({ kind: 'runtime', event: announce });
295
321
  }
322
+ const pending = this.pendingObservations;
323
+ this.pendingObservations = [];
324
+ for (const event of pending)
325
+ this.routeObservation(event);
296
326
  }
297
327
  async handleLifecycle(parsed) {
298
328
  if (!this.capabilities.has('msg_lifecycle_v1')) {
@@ -321,6 +351,9 @@ export class ClaudeProcessPump {
321
351
  kind: 'runtime',
322
352
  event: { type: 'error', message: `Claude input lifecycle update failed: ${String(err)}` },
323
353
  });
354
+ // failBestEffort may unregister this delivery before onFatal closes
355
+ // the process's remaining sinks. Its waiting drain still needs EOF.
356
+ delivery.sink.close();
324
357
  this.opts.hooks.onFatal();
325
358
  return;
326
359
  }
@@ -0,0 +1,4 @@
1
+ import { type RuntimeObservationEvent } from '@parall/agent-core';
2
+ /** Only facts the CLI emitted. A result UUID is not a native turn identity. */
3
+ export declare function claudeObservation(frame: Record<string, unknown>): RuntimeObservationEvent | undefined;
4
+ //# sourceMappingURL=runtime-observations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-observations.d.ts","sourceRoot":"","sources":["../src/runtime-observations.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,uBAAuB,EAC7B,MAAM,oBAAoB,CAAC;AAG5B,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,uBAAuB,GAAG,SAAS,CAqErC"}
@@ -0,0 +1,72 @@
1
+ import { observationField, runtimeObservation, } from '@parall/agent-core';
2
+ /** Only facts the CLI emitted. A result UUID is not a native turn identity. */
3
+ export function claudeObservation(frame) {
4
+ const sessionId = observationField(frame.session_id);
5
+ if (!sessionId || frame.parent_tool_use_id)
6
+ return;
7
+ const native = {
8
+ session_id: sessionId,
9
+ event_id: observationField(frame.uuid),
10
+ };
11
+ let event;
12
+ let status = 'unknown';
13
+ if (frame.type === 'result') {
14
+ event = 'request_completed';
15
+ status =
16
+ frame.is_error === true ? 'failed' : frame.is_error === false ? 'completed' : 'unknown';
17
+ native.status = observationField(frame.subtype);
18
+ if (typeof frame.num_turns === 'number' &&
19
+ Number.isSafeInteger(frame.num_turns) &&
20
+ frame.num_turns >= 0) {
21
+ native.num_turns = frame.num_turns;
22
+ }
23
+ }
24
+ else if (frame.type === 'system') {
25
+ switch (frame.subtype) {
26
+ case 'init':
27
+ event = 'session_initialized';
28
+ status = 'initialized';
29
+ break;
30
+ case 'hook_started':
31
+ case 'hook_response':
32
+ event = frame.subtype === 'hook_started' ? 'hook_started' : 'hook_completed';
33
+ status =
34
+ frame.subtype === 'hook_started'
35
+ ? 'running'
36
+ : frame.outcome === 'success'
37
+ ? 'completed'
38
+ : frame.outcome === 'error'
39
+ ? 'failed'
40
+ : frame.outcome === 'cancelled'
41
+ ? 'stopped'
42
+ : 'unknown';
43
+ native.hook_id = observationField(frame.hook_id);
44
+ native.hook_event = observationField(frame.hook_event);
45
+ native.status = observationField(frame.outcome);
46
+ break;
47
+ case 'status':
48
+ case 'session_state_changed':
49
+ event = 'session_status';
50
+ native.status = observationField(frame.status) ?? observationField(frame.state);
51
+ // Null does not prove the runtime is idle; only explicit known states
52
+ // can become public statuses.
53
+ if (native.status === 'compacting' ||
54
+ native.status === 'running' ||
55
+ native.status === 'idle') {
56
+ status = native.status;
57
+ }
58
+ break;
59
+ default:
60
+ return;
61
+ }
62
+ }
63
+ else
64
+ return;
65
+ return runtimeObservation(sessionId, {
66
+ runtime: 'claude-code',
67
+ source: 'stream_json',
68
+ event,
69
+ status,
70
+ native,
71
+ });
72
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"spawn-env.d.ts","sourceRoot":"","sources":["../src/spawn-env.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,CAAC,UAAU,EAC5B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,EAChC,IAAI,EAAE;IACJ,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,GACA,MAAM,CAAC,UAAU,CAiFnB"}
1
+ {"version":3,"file":"spawn-env.d.ts","sourceRoot":"","sources":["../src/spawn-env.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,CAAC,UAAU,EAC5B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,EAChC,IAAI,EAAE;IACJ,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,GACA,MAAM,CAAC,UAAU,CAoFnB"}
package/dist/spawn-env.js CHANGED
@@ -8,6 +8,9 @@ export function buildSpawnEnv(parentEnv, claudeHome, context, opts) {
8
8
  const result = {
9
9
  ...env,
10
10
  HOME: claudeHome,
11
+ // Request native state frames for observation without changing user files.
12
+ // Preserve an operator's explicit opt-out; older CLIs can ignore the flag.
13
+ CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: env.CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS ?? '1',
11
14
  PRLL_API_URL: context.apiUrl,
12
15
  PRLL_API_KEY: context.apiKey,
13
16
  PRLL_ORG_ID: context.orgId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/claude-agent",
3
- "version": "1.63.0",
3
+ "version": "1.65.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.65.0",
29
+ "@parall/cli": "1.65.0",
30
+ "@parall/sdk": "1.65.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,18 +674,18 @@ 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;
642
681
  if (this.processes.get(sessionKey) === state) this.processes.delete(sessionKey);
643
682
  this.retiring.delete(state);
644
683
  },
645
- // Main-session turns go to the gateway (steps + session activity);
646
- // fork-session turns only count as busy (fork-scope invariant).
647
684
  onRuntimeTurnOpened: (turn) =>
648
685
  this.activity.surfaceTurn(turn, this.opts.sessionManager.isMain(sessionKey), log),
649
686
  onRuntimeTurnClosed: () => this.maybeApplyRestart(sessionKey, state),
687
+ onObservation: (event) =>
688
+ this.activity.emit({ kind: 'observation', sessionKey, event }, log),
650
689
  },
651
690
  }),
652
691
  done: false,
@@ -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
  });
@@ -1,5 +1,6 @@
1
1
  import type { Readable } from 'node:stream';
2
2
  import type { RuntimeEvent } from '@parall/agent-core';
3
+ import { claudeObservation } from './runtime-observations.js';
3
4
 
4
5
  /**
5
6
  * Structured snapshot of the `result` frame's LLM-layer discriminators and
@@ -215,6 +216,9 @@ export async function* parseClaudeStreamJson(
215
216
  const now = Date.now();
216
217
  const eventTimestampMs = parseEventTimestampMs(event) ?? now;
217
218
  const eventRecord = event as Record<string, unknown>;
219
+ const observation = claudeObservation(eventRecord);
220
+ // Init must announce the native session before its observations drain.
221
+ if (observation && observation.content.event !== 'session_initialized') yield observation;
218
222
 
219
223
  if (eventRecord.type === 'system' && eventRecord.subtype === 'compact_boundary') {
220
224
  const meta =
@@ -244,6 +248,7 @@ export async function* parseClaudeStreamJson(
244
248
  ...(sessionId ? { sessionId } : {}),
245
249
  capabilities,
246
250
  };
251
+ if (observation) yield observation;
247
252
  continue;
248
253
  }
249
254
  const subtype = asTrimmedString(eventRecord.subtype) as
@@ -3,6 +3,7 @@ import {
3
3
  type GatewayLogger,
4
4
  type RuntimeBusyState,
5
5
  type RuntimeTurnTrigger,
6
+ type RuntimeObservationEvent,
6
7
  } from '@parall/agent-core';
7
8
  import { ClaudeBusyTracker } from './busy-state.js';
8
9
  import type { ClaudeInputDelivery, ClaudeInputRegistry } from './input-lifecycle.js';
@@ -24,6 +25,7 @@ export type ClaudeProcessPumpHooks = {
24
25
  onEof(): void;
25
26
  onRuntimeTurnOpened(turn: ClaudeRuntimeTurn): void;
26
27
  onRuntimeTurnClosed(): void;
28
+ onObservation?(event: RuntimeObservationEvent): void;
27
29
  };
28
30
 
29
31
  export type ClaudeProcessPumpOptions = {
@@ -67,6 +69,7 @@ export class ClaudeProcessPump {
67
69
  private idleTimer: NodeJS.Timeout | null = null;
68
70
  private activeDrain: ClaudeInputDelivery | null = null;
69
71
  private readonly now: () => number;
72
+ private pendingObservations: RuntimeObservationEvent[] = [];
70
73
 
71
74
  constructor(private readonly opts: ClaudeProcessPumpOptions) {
72
75
  this.now = opts.now ?? Date.now;
@@ -194,7 +197,7 @@ export class ClaudeProcessPump {
194
197
  try {
195
198
  for await (const parsed of parser) {
196
199
  if (this.closed) break;
197
- this.touchAll();
200
+ if (parsed.type !== 'observation') this.touchAll();
198
201
  await this.route(parsed);
199
202
  }
200
203
  } catch (err) {
@@ -208,6 +211,9 @@ export class ClaudeProcessPump {
208
211
  switch (parsed.type) {
209
212
  case 'runtime_activity':
210
213
  return;
214
+ case 'observation':
215
+ this.routeObservation(parsed);
216
+ return;
211
217
  case 'runtime_init':
212
218
  this.handleInit(parsed);
213
219
  return;
@@ -261,6 +267,25 @@ export class ClaudeProcessPump {
261
267
  this.runtimeTurn?.touch();
262
268
  }
263
269
 
270
+ private routeObservation(event: RuntimeObservationEvent): void {
271
+ if (!this.sessionId) {
272
+ // SessionStart hooks precede init. Keep a bounded startup buffer and
273
+ // release only records whose native session matches the eventual init.
274
+ if (this.pendingObservations.length < 64) this.pendingObservations.push(event);
275
+ else this.opts.log?.warn('Claude startup observation buffer full; dropping observation');
276
+ return;
277
+ }
278
+ if (event.runtimeSessionId !== this.sessionId) {
279
+ this.opts.log?.warn('Claude observation does not match the initialized session');
280
+ return;
281
+ }
282
+ const owner =
283
+ this.ownerFor(false) ??
284
+ (this.activeDrain && !this.activeDrain.terminal ? this.activeDrain : null);
285
+ if (owner) owner.sink.push({ kind: 'runtime', event });
286
+ else this.opts.hooks.onObservation?.(event);
287
+ }
288
+
264
289
  private liveDeliveries(): ClaudeInputDelivery[] {
265
290
  const live: ClaudeInputDelivery[] = [];
266
291
  for (const delivery of this.opts.inputs.values()) if (isLive(delivery)) live.push(delivery);
@@ -328,6 +353,9 @@ export class ClaudeProcessPump {
328
353
  delivery.sessionAnnounced = true;
329
354
  delivery.sink.push({ kind: 'runtime', event: announce });
330
355
  }
356
+ const pending = this.pendingObservations;
357
+ this.pendingObservations = [];
358
+ for (const event of pending) this.routeObservation(event);
331
359
  }
332
360
 
333
361
  private async handleLifecycle(parsed: {
@@ -361,6 +389,9 @@ export class ClaudeProcessPump {
361
389
  kind: 'runtime',
362
390
  event: { type: 'error', message: `Claude input lifecycle update failed: ${String(err)}` },
363
391
  });
392
+ // failBestEffort may unregister this delivery before onFatal closes
393
+ // the process's remaining sinks. Its waiting drain still needs EOF.
394
+ delivery.sink.close();
364
395
  this.opts.hooks.onFatal();
365
396
  return;
366
397
  }
@@ -0,0 +1,80 @@
1
+ import {
2
+ observationField,
3
+ runtimeObservation,
4
+ type RuntimeObservationEvent,
5
+ } from '@parall/agent-core';
6
+ import type { RuntimeObservationContent } from '@parall/sdk';
7
+
8
+ /** Only facts the CLI emitted. A result UUID is not a native turn identity. */
9
+ export function claudeObservation(
10
+ frame: Record<string, unknown>,
11
+ ): RuntimeObservationEvent | undefined {
12
+ const sessionId = observationField(frame.session_id);
13
+ if (!sessionId || frame.parent_tool_use_id) return;
14
+ const native: NonNullable<RuntimeObservationContent['native']> = {
15
+ session_id: sessionId,
16
+ event_id: observationField(frame.uuid),
17
+ };
18
+ let event: RuntimeObservationContent['event'];
19
+ let status: RuntimeObservationContent['status'] = 'unknown';
20
+ if (frame.type === 'result') {
21
+ event = 'request_completed';
22
+ status =
23
+ frame.is_error === true ? 'failed' : frame.is_error === false ? 'completed' : 'unknown';
24
+ native.status = observationField(frame.subtype);
25
+ if (
26
+ typeof frame.num_turns === 'number' &&
27
+ Number.isSafeInteger(frame.num_turns) &&
28
+ frame.num_turns >= 0
29
+ ) {
30
+ native.num_turns = frame.num_turns;
31
+ }
32
+ } else if (frame.type === 'system') {
33
+ switch (frame.subtype) {
34
+ case 'init':
35
+ event = 'session_initialized';
36
+ status = 'initialized';
37
+ break;
38
+ case 'hook_started':
39
+ case 'hook_response':
40
+ event = frame.subtype === 'hook_started' ? 'hook_started' : 'hook_completed';
41
+ status =
42
+ frame.subtype === 'hook_started'
43
+ ? 'running'
44
+ : frame.outcome === 'success'
45
+ ? 'completed'
46
+ : frame.outcome === 'error'
47
+ ? 'failed'
48
+ : frame.outcome === 'cancelled'
49
+ ? 'stopped'
50
+ : 'unknown';
51
+ native.hook_id = observationField(frame.hook_id);
52
+ native.hook_event = observationField(frame.hook_event);
53
+ native.status = observationField(frame.outcome);
54
+ break;
55
+ case 'status':
56
+ case 'session_state_changed':
57
+ event = 'session_status';
58
+ native.status = observationField(frame.status) ?? observationField(frame.state);
59
+ // Null does not prove the runtime is idle; only explicit known states
60
+ // can become public statuses.
61
+ if (
62
+ native.status === 'compacting' ||
63
+ native.status === 'running' ||
64
+ native.status === 'idle'
65
+ ) {
66
+ status = native.status;
67
+ }
68
+ break;
69
+ default:
70
+ return;
71
+ }
72
+ } else return;
73
+ return runtimeObservation(sessionId, {
74
+ runtime: 'claude-code',
75
+ source: 'stream_json',
76
+ event,
77
+ status,
78
+ native,
79
+ });
80
+ }
package/src/spawn-env.ts CHANGED
@@ -22,6 +22,9 @@ export function buildSpawnEnv(
22
22
  const result: NodeJS.ProcessEnv = {
23
23
  ...env,
24
24
  HOME: claudeHome,
25
+ // Request native state frames for observation without changing user files.
26
+ // Preserve an operator's explicit opt-out; older CLIs can ignore the flag.
27
+ CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: env.CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS ?? '1',
25
28
  PRLL_API_URL: context.apiUrl,
26
29
  PRLL_API_KEY: context.apiKey,
27
30
  PRLL_ORG_ID: context.orgId,