@parall/claude-agent 1.62.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.
- package/dist/dispatch.d.ts +18 -8
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +54 -16
- package/dist/index.js +22 -4
- package/dist/input-lifecycle.d.ts +3 -3
- package/dist/input-lifecycle.d.ts.map +1 -1
- package/dist/input-lifecycle.js +13 -6
- package/dist/process-pump.d.ts.map +1 -1
- package/dist/process-pump.js +8 -2
- package/package.json +4 -4
- package/src/dispatch.ts +65 -23
- package/src/index.ts +24 -3
- package/src/input-lifecycle.ts +21 -6
- package/src/process-pump.ts +8 -2
package/dist/dispatch.d.ts
CHANGED
|
@@ -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';
|
|
@@ -18,12 +18,8 @@ type ClaudeCodeAdapterOptions = Pick<ClaudeAgentConfig, 'additionalDirs' | 'allo
|
|
|
18
18
|
/** @deprecated Use contextFilePathForSession. */
|
|
19
19
|
stepIdFilePathForSession?: (sessionKey: string) => string;
|
|
20
20
|
/**
|
|
21
|
-
* PRLL_CONTEXT_DIR root
|
|
22
|
-
*
|
|
23
|
-
* subprocess env is built ONCE from the placeholder context, so a value
|
|
24
|
-
* only present on per-dispatch DispatchContexts would never reach the
|
|
25
|
-
* CLI and every reply would silently lose its lane binding (turns would
|
|
26
|
-
* resolve no_action instead of reply-effect dedupe).
|
|
21
|
+
* Bridge-static PRLL_CONTEXT_DIR root: a value only in per-dispatch context
|
|
22
|
+
* never reaches the long-lived CLI, so replies lose their lane binding.
|
|
27
23
|
*/
|
|
28
24
|
contextDirPath?: string;
|
|
29
25
|
/**
|
|
@@ -64,6 +60,7 @@ export type ProcessState = {
|
|
|
64
60
|
export declare class ClaudeCodeAdapter implements DispatchAdapter {
|
|
65
61
|
private readonly opts;
|
|
66
62
|
readonly inputLifecycleMode: "explicit";
|
|
63
|
+
readonly inputSettlementMode: "receipt";
|
|
67
64
|
private readonly processes;
|
|
68
65
|
/** Retired by abortDispatch while the CLI still had its own work: busy until idle, then terminated. */
|
|
69
66
|
private readonly retiring;
|
|
@@ -92,7 +89,19 @@ export declare class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
92
89
|
abortDispatch(sessionKey: string): void;
|
|
93
90
|
hasPendingInjections(sessionKey: string): boolean;
|
|
94
91
|
hasUnsettledInjections(sessionKey: string): boolean;
|
|
95
|
-
|
|
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>;
|
|
96
105
|
dispatch({ event, bodyForAgent, sessionKey, context, inputLifecycle, noteActivity, }: DispatchOpts): AsyncIterable<RuntimeEvent>;
|
|
97
106
|
getBranchPoint(_sessionKey: string): string | undefined;
|
|
98
107
|
getSessionHistoryPath(sessionKey: string): string | undefined;
|
|
@@ -129,6 +138,7 @@ export declare class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
129
138
|
private maybeApplyRestart;
|
|
130
139
|
private ensureProcess;
|
|
131
140
|
private spawnProcess;
|
|
141
|
+
/** Resolves once every live input's failed report has settled (best effort). */
|
|
132
142
|
private killProcess;
|
|
133
143
|
private endStdin;
|
|
134
144
|
private terminateHandle;
|
package/dist/dispatch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACb,eAAe,EACf,sBAAsB,EACtB,YAAY,EACZ,QAAQ,EACR,aAAa,EACb,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACb,MAAM,oBAAoB,CAAC;AAQ5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAA4B,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAErF,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAG/C,OAAO,EAAE,aAAa,EAAE,CAAC;AAEzB,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,mDAAmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yBAAyB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3D,iDAAiD;IACjD,wBAAwB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1D
|
|
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
|
-
|
|
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.
|
|
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];
|
|
@@ -282,7 +311,10 @@ export class ClaudeCodeAdapter {
|
|
|
282
311
|
}
|
|
283
312
|
catch (err) {
|
|
284
313
|
try {
|
|
285
|
-
await lifecycle?.update('failed'
|
|
314
|
+
await lifecycle?.update('failed', {
|
|
315
|
+
outcomeClass: 'runtime_crash',
|
|
316
|
+
detail: `Claude spawn failed: ${String(err)}`,
|
|
317
|
+
});
|
|
286
318
|
}
|
|
287
319
|
catch (reportErr) {
|
|
288
320
|
log?.warn?.(`failed to report rejected Claude input as failed: ${String(reportErr)}`);
|
|
@@ -297,10 +329,10 @@ export class ClaudeCodeAdapter {
|
|
|
297
329
|
this.writeUserMessage(state.handle, promptBody, delivery.commandUuid);
|
|
298
330
|
}
|
|
299
331
|
catch (err) {
|
|
300
|
-
await state.inputs.failBestEffort(delivery, log);
|
|
332
|
+
await state.inputs.failBestEffort(delivery, log, `Claude stdin write failed: ${String(err)}`);
|
|
301
333
|
state.inputs.remove(delivery);
|
|
302
334
|
yield { type: 'error', message: `Claude stdin write failed: ${String(err)}` };
|
|
303
|
-
this.killProcess(sessionKey, state);
|
|
335
|
+
void this.killProcess(sessionKey, state);
|
|
304
336
|
return;
|
|
305
337
|
}
|
|
306
338
|
try {
|
|
@@ -315,7 +347,9 @@ export class ClaudeCodeAdapter {
|
|
|
315
347
|
return runClaudeCompact({
|
|
316
348
|
ensureRuntimeCapability: (log) => this.ensureRuntimeCapability(log),
|
|
317
349
|
ensureProcess: (sessionKey, log) => this.ensureProcess(sessionKey, log),
|
|
318
|
-
killProcess: (sessionKey, state) =>
|
|
350
|
+
killProcess: (sessionKey, state) => {
|
|
351
|
+
void this.killProcess(sessionKey, state);
|
|
352
|
+
},
|
|
319
353
|
writeUserMessage: (handle, text, uuid) => this.writeUserMessage(handle, text, uuid),
|
|
320
354
|
applyPendingRestart: (sessionKey, state) => this.maybeApplyRestart(sessionKey, state),
|
|
321
355
|
}, opts);
|
|
@@ -448,7 +482,7 @@ export class ClaudeCodeAdapter {
|
|
|
448
482
|
if (outstanding.total > 0) {
|
|
449
483
|
state.log?.warn?.(`restarting ${sessionKey} with ${outstanding.total} live background task(s); they die with the process`);
|
|
450
484
|
}
|
|
451
|
-
this.killProcess(sessionKey, state);
|
|
485
|
+
void this.killProcess(sessionKey, state);
|
|
452
486
|
}
|
|
453
487
|
ensureProcess(sessionKey, log) {
|
|
454
488
|
if (this.shuttingDown) {
|
|
@@ -463,7 +497,7 @@ export class ClaudeCodeAdapter {
|
|
|
463
497
|
log?.info?.(`deferring lazy restart of ${sessionKey}: runtime-initiated turn in progress`);
|
|
464
498
|
return existing;
|
|
465
499
|
}
|
|
466
|
-
this.killProcess(sessionKey, existing);
|
|
500
|
+
void this.killProcess(sessionKey, existing);
|
|
467
501
|
}
|
|
468
502
|
else if (alive) {
|
|
469
503
|
return existing;
|
|
@@ -498,7 +532,7 @@ export class ClaudeCodeAdapter {
|
|
|
498
532
|
onFatal: () => {
|
|
499
533
|
const current = this.processes.get(sessionKey);
|
|
500
534
|
if (current === state)
|
|
501
|
-
this.killProcess(sessionKey, state);
|
|
535
|
+
void this.killProcess(sessionKey, state);
|
|
502
536
|
},
|
|
503
537
|
onEof: () => {
|
|
504
538
|
state.done = true;
|
|
@@ -546,7 +580,8 @@ export class ClaudeCodeAdapter {
|
|
|
546
580
|
proc.stderr.on('data', (chunk) => {
|
|
547
581
|
stderrChunks.push(chunk.toString());
|
|
548
582
|
});
|
|
549
|
-
proc.stdin.on('error', () => {
|
|
583
|
+
proc.stdin.on('error', (err) => {
|
|
584
|
+
stderrChunks.push(`Claude stdin failed: ${String(err)}\n`);
|
|
550
585
|
proc.stdin.destroy();
|
|
551
586
|
if (proc.exitCode === null && proc.signalCode === null) {
|
|
552
587
|
if (!IS_WIN32 || !proc.pid || !killWin32Tree(proc.pid)) {
|
|
@@ -560,10 +595,12 @@ export class ClaudeCodeAdapter {
|
|
|
560
595
|
});
|
|
561
596
|
return { proc, exitPromise, stderrChunks };
|
|
562
597
|
}
|
|
598
|
+
/** Resolves once every live input's failed report has settled (best effort). */
|
|
563
599
|
killProcess(sessionKey, state) {
|
|
600
|
+
const reports = [];
|
|
564
601
|
for (const delivery of state.inputs.values()) {
|
|
565
602
|
if (!delivery.terminal)
|
|
566
|
-
|
|
603
|
+
reports.push(state.inputs.failBestEffort(delivery, state.log));
|
|
567
604
|
}
|
|
568
605
|
state.done = true;
|
|
569
606
|
const current = this.processes.get(sessionKey);
|
|
@@ -572,6 +609,7 @@ export class ClaudeCodeAdapter {
|
|
|
572
609
|
}
|
|
573
610
|
state.pump.close('killed', 'Claude process terminated by the bridge');
|
|
574
611
|
this.terminateHandle(state.handle);
|
|
612
|
+
return Promise.all(reports).then(() => undefined);
|
|
575
613
|
}
|
|
576
614
|
endStdin(handle) {
|
|
577
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
|
-
|
|
304
|
-
|
|
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
|
});
|
|
@@ -63,8 +63,8 @@ export declare class ClaudeInputRegistry {
|
|
|
63
63
|
*/
|
|
64
64
|
discardBookkeeping(deliveryKey: string): void;
|
|
65
65
|
apply(delivery: ClaudeInputDelivery, state: 'queued' | 'started' | 'completed' | 'cancelled' | 'discarded'): Promise<void>;
|
|
66
|
-
fail(delivery: ClaudeInputDelivery): Promise<void>;
|
|
67
|
-
failBestEffort(delivery: ClaudeInputDelivery, log?: GatewayLogger): Promise<void>;
|
|
68
|
-
failAllBestEffort(log?: GatewayLogger): Promise<void>;
|
|
66
|
+
fail(delivery: ClaudeInputDelivery, detail?: string): Promise<void>;
|
|
67
|
+
failBestEffort(delivery: ClaudeInputDelivery, log?: GatewayLogger, detail?: string): Promise<void>;
|
|
68
|
+
failAllBestEffort(log?: GatewayLogger, detail?: string): Promise<void>;
|
|
69
69
|
}
|
|
70
70
|
//# sourceMappingURL=input-lifecycle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input-lifecycle.d.ts","sourceRoot":"","sources":["../src/input-lifecycle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"input-lifecycle.d.ts","sourceRoot":"","sources":["../src/input-lifecycle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEnG,OAAO,EACL,KAAK,cAAc,EAGnB,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC9C,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,YAAY,EAAE,OAAO,CAAC;IACtB,kFAAkF;IAClF,IAAI,EAAE,cAAc,CAAC;IACrB,uEAAuE;IACvE,QAAQ,EAAE,YAAY,CAAC;IACvB,6FAA6F;IAC7F,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,8DAA8D;IAC9D,gBAAgB,EAAE,OAAO,CAAC;IAC1B;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAEF;;;;GAIG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0C;IAChE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;IAEpE,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAI9D,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAIlE,eAAe,CAAC,gBAAgB,EAAE,MAAM,EAAE,GAAG,mBAAmB,GAAG,SAAS;IAU5E,MAAM,IAAI,gBAAgB,CAAC,mBAAmB,CAAC;IAI/C,oBAAoB,IAAI,OAAO;IAI/B;;;;;;;;;OASG;IACH,sBAAsB,IAAI,OAAO;IAIjC,QAAQ,CACN,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,sBAAsB,GAAG,SAAS,EAC7C,QAAQ,EAAE,OAAO,EACjB,YAAY,CAAC,EAAE,MAAM,IAAI,EACzB,GAAG,CAAC,EAAE,aAAa,GAClB,mBAAmB;IAsBtB,MAAM,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAY3C;;;;;;;;OAQG;IACH,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAOvC,KAAK,CACT,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACpE,OAAO,CAAC,IAAI,CAAC;IA0BV,IAAI,CAAC,QAAQ,EAAE,mBAAmB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA6BnE,cAAc,CAClB,QAAQ,EAAE,mBAAmB,EAC7B,GAAG,CAAC,EAAE,aAAa,EACnB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC;IAUV,iBAAiB,CAAC,GAAG,CAAC,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAO7E"}
|
package/dist/input-lifecycle.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { classifyClaudeTurn } from './turn-outcome.js';
|
|
2
3
|
import { newTurnEvidence, newTurnSink, } from './turn-sink.js';
|
|
3
4
|
/**
|
|
4
5
|
* Per-process registry that maps Parall WorkItem batches to Claude stdin
|
|
@@ -116,7 +117,7 @@ export class ClaudeInputRegistry {
|
|
|
116
117
|
if (delivery.drained)
|
|
117
118
|
this.remove(delivery);
|
|
118
119
|
}
|
|
119
|
-
async fail(delivery) {
|
|
120
|
+
async fail(delivery, detail) {
|
|
120
121
|
if (delivery.terminal)
|
|
121
122
|
return;
|
|
122
123
|
if (delivery.suppressFailReport) {
|
|
@@ -127,7 +128,13 @@ export class ClaudeInputRegistry {
|
|
|
127
128
|
}
|
|
128
129
|
try {
|
|
129
130
|
if (delivery.reportedState !== 'failed') {
|
|
130
|
-
const
|
|
131
|
+
const classified = classifyClaudeTurn(delivery.evidence.lastResultMeta, delivery.evidence.noticeTexts);
|
|
132
|
+
const result = await delivery.lifecycle?.update('failed', {
|
|
133
|
+
outcomeClass: classified.outcome === 'ok' || classified.outcome === 'usage_limit'
|
|
134
|
+
? 'runtime_crash'
|
|
135
|
+
: classified.outcome,
|
|
136
|
+
detail: detail ?? classified.detail ?? 'Claude input failed',
|
|
137
|
+
});
|
|
131
138
|
delivery.reportedState = 'failed';
|
|
132
139
|
delivery.terminal = result?.retry === false ? 'settled' : 'failed';
|
|
133
140
|
}
|
|
@@ -138,17 +145,17 @@ export class ClaudeInputRegistry {
|
|
|
138
145
|
this.remove(delivery);
|
|
139
146
|
}
|
|
140
147
|
}
|
|
141
|
-
async failBestEffort(delivery, log) {
|
|
148
|
+
async failBestEffort(delivery, log, detail) {
|
|
142
149
|
try {
|
|
143
|
-
await this.fail(delivery);
|
|
150
|
+
await this.fail(delivery, detail);
|
|
144
151
|
}
|
|
145
152
|
catch (err) {
|
|
146
153
|
log?.warn?.(`failed to report Claude input ${delivery.commandUuid} as failed: ${String(err)}`);
|
|
147
154
|
}
|
|
148
155
|
}
|
|
149
|
-
async failAllBestEffort(log) {
|
|
156
|
+
async failAllBestEffort(log, detail) {
|
|
150
157
|
await Promise.all([...this.byKey.values()]
|
|
151
158
|
.filter((delivery) => !delivery.terminal)
|
|
152
|
-
.map((delivery) => this.failBestEffort(delivery, log)));
|
|
159
|
+
.map((delivery) => this.failBestEffort(delivery, log, detail)));
|
|
153
160
|
}
|
|
154
161
|
}
|
|
@@ -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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"}
|
package/dist/process-pump.js
CHANGED
|
@@ -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
|
}
|
|
@@ -417,8 +420,11 @@ export class ClaudeProcessPump {
|
|
|
417
420
|
if (!delivery.terminal && settledAsLimit(delivery.evidence))
|
|
418
421
|
delivery.suppressFailReport = true;
|
|
419
422
|
}
|
|
420
|
-
|
|
421
|
-
for
|
|
423
|
+
// Failing removes drained deliveries from the registry. Their sinks must
|
|
424
|
+
// still receive EOF, or a startup failure waits for the dispatch timeout.
|
|
425
|
+
const deliveries = [...inputs.values()];
|
|
426
|
+
await inputs.failAllBestEffort(log, message);
|
|
427
|
+
for (const delivery of deliveries) {
|
|
422
428
|
delivery.sink.push({ kind: 'ended', reason: 'eof', message });
|
|
423
429
|
delivery.sink.close();
|
|
424
430
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/claude-agent",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
29
|
-
"@parall/cli": "1.
|
|
30
|
-
"@parall/sdk": "1.
|
|
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,
|
|
@@ -57,12 +58,8 @@ type ClaudeCodeAdapterOptions = Pick<
|
|
|
57
58
|
/** @deprecated Use contextFilePathForSession. */
|
|
58
59
|
stepIdFilePathForSession?: (sessionKey: string) => string;
|
|
59
60
|
/**
|
|
60
|
-
* PRLL_CONTEXT_DIR root
|
|
61
|
-
*
|
|
62
|
-
* subprocess env is built ONCE from the placeholder context, so a value
|
|
63
|
-
* only present on per-dispatch DispatchContexts would never reach the
|
|
64
|
-
* CLI and every reply would silently lose its lane binding (turns would
|
|
65
|
-
* resolve no_action instead of reply-effect dedupe).
|
|
61
|
+
* Bridge-static PRLL_CONTEXT_DIR root: a value only in per-dispatch context
|
|
62
|
+
* never reaches the long-lived CLI, so replies lose their lane binding.
|
|
66
63
|
*/
|
|
67
64
|
contextDirPath?: string;
|
|
68
65
|
/**
|
|
@@ -123,6 +120,7 @@ export type ProcessState = {
|
|
|
123
120
|
|
|
124
121
|
export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
125
122
|
readonly inputLifecycleMode = 'explicit' as const;
|
|
123
|
+
readonly inputSettlementMode = 'receipt' as const;
|
|
126
124
|
private readonly processes = new Map<string, ProcessState>();
|
|
127
125
|
/** Retired by abortDispatch while the CLI still had its own work: busy until idle, then terminated. */
|
|
128
126
|
private readonly retiring = new Set<ProcessState>();
|
|
@@ -264,15 +262,50 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
264
262
|
return state.inputs.hasUnsettledInjections();
|
|
265
263
|
}
|
|
266
264
|
|
|
267
|
-
|
|
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> {
|
|
268
281
|
const state = this.processes.get(sessionKey);
|
|
269
|
-
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
|
+
}
|
|
270
304
|
state.inputs.discardBookkeeping(deliveryKey);
|
|
271
305
|
// The discarded copy may have been the last thing a lazy restart was
|
|
272
|
-
// waiting on.
|
|
273
|
-
// still be RUNNING (non-terminal): killing the process now would fail a
|
|
274
|
-
// live input, so the restart also waits for every injection to settle.
|
|
306
|
+
// waiting on.
|
|
275
307
|
this.maybeApplyRestart(sessionKey, state);
|
|
308
|
+
return 'discarded';
|
|
276
309
|
}
|
|
277
310
|
|
|
278
311
|
async *dispatch({
|
|
@@ -364,14 +397,14 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
364
397
|
cleanupFork({ fork }: CleanupForkOpts) {
|
|
365
398
|
const state = this.processes.get(fork.sessionKey);
|
|
366
399
|
if (state) {
|
|
367
|
-
this.killProcess(fork.sessionKey, state);
|
|
400
|
+
void this.killProcess(fork.sessionKey, state);
|
|
368
401
|
}
|
|
369
402
|
this.opts.sessionManager.cleanupFork(fork.sessionKey);
|
|
370
403
|
}
|
|
371
404
|
|
|
372
405
|
resetProcesses(): void {
|
|
373
406
|
for (const [sessionKey, state] of this.processes) {
|
|
374
|
-
this.killProcess(sessionKey, state);
|
|
407
|
+
void this.killProcess(sessionKey, state);
|
|
375
408
|
}
|
|
376
409
|
this.processes.clear();
|
|
377
410
|
const retired = [...this.retiring];
|
|
@@ -410,7 +443,10 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
410
443
|
state = this.ensureProcess(sessionKey, log);
|
|
411
444
|
} catch (err) {
|
|
412
445
|
try {
|
|
413
|
-
await lifecycle?.update('failed'
|
|
446
|
+
await lifecycle?.update('failed', {
|
|
447
|
+
outcomeClass: 'runtime_crash',
|
|
448
|
+
detail: `Claude spawn failed: ${String(err)}`,
|
|
449
|
+
});
|
|
414
450
|
} catch (reportErr) {
|
|
415
451
|
log?.warn?.(`failed to report rejected Claude input as failed: ${String(reportErr)}`);
|
|
416
452
|
}
|
|
@@ -423,10 +459,10 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
423
459
|
try {
|
|
424
460
|
this.writeUserMessage(state.handle, promptBody, delivery.commandUuid);
|
|
425
461
|
} catch (err) {
|
|
426
|
-
await state.inputs.failBestEffort(delivery, log);
|
|
462
|
+
await state.inputs.failBestEffort(delivery, log, `Claude stdin write failed: ${String(err)}`);
|
|
427
463
|
state.inputs.remove(delivery);
|
|
428
464
|
yield { type: 'error', message: `Claude stdin write failed: ${String(err)}` };
|
|
429
|
-
this.killProcess(sessionKey, state);
|
|
465
|
+
void this.killProcess(sessionKey, state);
|
|
430
466
|
return;
|
|
431
467
|
}
|
|
432
468
|
|
|
@@ -443,7 +479,9 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
443
479
|
{
|
|
444
480
|
ensureRuntimeCapability: (log) => this.ensureRuntimeCapability(log),
|
|
445
481
|
ensureProcess: (sessionKey, log) => this.ensureProcess(sessionKey, log),
|
|
446
|
-
killProcess: (sessionKey, state) =>
|
|
482
|
+
killProcess: (sessionKey, state) => {
|
|
483
|
+
void this.killProcess(sessionKey, state);
|
|
484
|
+
},
|
|
447
485
|
writeUserMessage: (handle, text, uuid) => this.writeUserMessage(handle, text, uuid),
|
|
448
486
|
applyPendingRestart: (sessionKey, state) => this.maybeApplyRestart(sessionKey, state),
|
|
449
487
|
},
|
|
@@ -584,7 +622,7 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
584
622
|
`restarting ${sessionKey} with ${outstanding.total} live background task(s); they die with the process`,
|
|
585
623
|
);
|
|
586
624
|
}
|
|
587
|
-
this.killProcess(sessionKey, state);
|
|
625
|
+
void this.killProcess(sessionKey, state);
|
|
588
626
|
}
|
|
589
627
|
|
|
590
628
|
private ensureProcess(sessionKey: string, log: GatewayLogger | undefined): ProcessState {
|
|
@@ -603,7 +641,7 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
603
641
|
);
|
|
604
642
|
return existing;
|
|
605
643
|
}
|
|
606
|
-
this.killProcess(sessionKey, existing);
|
|
644
|
+
void this.killProcess(sessionKey, existing);
|
|
607
645
|
} else if (alive) {
|
|
608
646
|
return existing;
|
|
609
647
|
} else {
|
|
@@ -636,7 +674,7 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
636
674
|
},
|
|
637
675
|
onFatal: () => {
|
|
638
676
|
const current = this.processes.get(sessionKey);
|
|
639
|
-
if (current === state) this.killProcess(sessionKey, state);
|
|
677
|
+
if (current === state) void this.killProcess(sessionKey, state);
|
|
640
678
|
},
|
|
641
679
|
onEof: () => {
|
|
642
680
|
state.done = true;
|
|
@@ -704,7 +742,8 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
704
742
|
proc.stderr.on('data', (chunk: Buffer) => {
|
|
705
743
|
stderrChunks.push(chunk.toString());
|
|
706
744
|
});
|
|
707
|
-
proc.stdin.on('error', () => {
|
|
745
|
+
proc.stdin.on('error', (err) => {
|
|
746
|
+
stderrChunks.push(`Claude stdin failed: ${String(err)}\n`);
|
|
708
747
|
proc.stdin.destroy();
|
|
709
748
|
if (proc.exitCode === null && proc.signalCode === null) {
|
|
710
749
|
if (!IS_WIN32 || !proc.pid || !killWin32Tree(proc.pid)) {
|
|
@@ -723,9 +762,11 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
723
762
|
return { proc, exitPromise, stderrChunks };
|
|
724
763
|
}
|
|
725
764
|
|
|
726
|
-
|
|
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)
|
|
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
|
-
|
|
369
|
-
|
|
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
|
});
|
package/src/input-lifecycle.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import type { DispatchInputLifecycle, GatewayLogger, RuntimeInputState } from '@parall/agent-core';
|
|
3
|
+
import { classifyClaudeTurn } from './turn-outcome.js';
|
|
3
4
|
import {
|
|
4
5
|
type ClaudeTurnSink,
|
|
5
6
|
newTurnEvidence,
|
|
@@ -168,7 +169,7 @@ export class ClaudeInputRegistry {
|
|
|
168
169
|
if (delivery.drained) this.remove(delivery);
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
async fail(delivery: ClaudeInputDelivery): Promise<void> {
|
|
172
|
+
async fail(delivery: ClaudeInputDelivery, detail?: string): Promise<void> {
|
|
172
173
|
if (delivery.terminal) return;
|
|
173
174
|
if (delivery.suppressFailReport) {
|
|
174
175
|
delivery.terminal = 'settled';
|
|
@@ -177,7 +178,17 @@ export class ClaudeInputRegistry {
|
|
|
177
178
|
}
|
|
178
179
|
try {
|
|
179
180
|
if (delivery.reportedState !== 'failed') {
|
|
180
|
-
const
|
|
181
|
+
const classified = classifyClaudeTurn(
|
|
182
|
+
delivery.evidence.lastResultMeta,
|
|
183
|
+
delivery.evidence.noticeTexts,
|
|
184
|
+
);
|
|
185
|
+
const result = await delivery.lifecycle?.update('failed', {
|
|
186
|
+
outcomeClass:
|
|
187
|
+
classified.outcome === 'ok' || classified.outcome === 'usage_limit'
|
|
188
|
+
? 'runtime_crash'
|
|
189
|
+
: classified.outcome,
|
|
190
|
+
detail: detail ?? classified.detail ?? 'Claude input failed',
|
|
191
|
+
});
|
|
181
192
|
delivery.reportedState = 'failed';
|
|
182
193
|
delivery.terminal = result?.retry === false ? 'settled' : 'failed';
|
|
183
194
|
}
|
|
@@ -187,9 +198,13 @@ export class ClaudeInputRegistry {
|
|
|
187
198
|
}
|
|
188
199
|
}
|
|
189
200
|
|
|
190
|
-
async failBestEffort(
|
|
201
|
+
async failBestEffort(
|
|
202
|
+
delivery: ClaudeInputDelivery,
|
|
203
|
+
log?: GatewayLogger,
|
|
204
|
+
detail?: string,
|
|
205
|
+
): Promise<void> {
|
|
191
206
|
try {
|
|
192
|
-
await this.fail(delivery);
|
|
207
|
+
await this.fail(delivery, detail);
|
|
193
208
|
} catch (err) {
|
|
194
209
|
log?.warn?.(
|
|
195
210
|
`failed to report Claude input ${delivery.commandUuid} as failed: ${String(err)}`,
|
|
@@ -197,11 +212,11 @@ export class ClaudeInputRegistry {
|
|
|
197
212
|
}
|
|
198
213
|
}
|
|
199
214
|
|
|
200
|
-
async failAllBestEffort(log?: GatewayLogger): Promise<void> {
|
|
215
|
+
async failAllBestEffort(log?: GatewayLogger, detail?: string): Promise<void> {
|
|
201
216
|
await Promise.all(
|
|
202
217
|
[...this.byKey.values()]
|
|
203
218
|
.filter((delivery) => !delivery.terminal)
|
|
204
|
-
.map((delivery) => this.failBestEffort(delivery, log)),
|
|
219
|
+
.map((delivery) => this.failBestEffort(delivery, log, detail)),
|
|
205
220
|
);
|
|
206
221
|
}
|
|
207
222
|
}
|
package/src/process-pump.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -470,8 +473,11 @@ export class ClaudeProcessPump {
|
|
|
470
473
|
if (!delivery.terminal && settledAsLimit(delivery.evidence))
|
|
471
474
|
delivery.suppressFailReport = true;
|
|
472
475
|
}
|
|
473
|
-
|
|
474
|
-
for
|
|
476
|
+
// Failing removes drained deliveries from the registry. Their sinks must
|
|
477
|
+
// still receive EOF, or a startup failure waits for the dispatch timeout.
|
|
478
|
+
const deliveries = [...inputs.values()];
|
|
479
|
+
await inputs.failAllBestEffort(log, message);
|
|
480
|
+
for (const delivery of deliveries) {
|
|
475
481
|
delivery.sink.push({ kind: 'ended', reason: 'eof', message });
|
|
476
482
|
delivery.sink.close();
|
|
477
483
|
}
|