@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.
- package/dist/dispatch.d.ts +16 -2
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +48 -15
- package/dist/index.js +22 -4
- package/dist/output-parser.d.ts.map +1 -1
- package/dist/output-parser.js +7 -0
- package/dist/process-pump.d.ts +4 -1
- package/dist/process-pump.d.ts.map +1 -1
- package/dist/process-pump.js +34 -1
- package/dist/runtime-observations.d.ts +4 -0
- package/dist/runtime-observations.d.ts.map +1 -0
- package/dist/runtime-observations.js +72 -0
- package/dist/spawn-env.d.ts.map +1 -1
- package/dist/spawn-env.js +3 -0
- package/package.json +4 -4
- package/src/dispatch.ts +58 -16
- package/src/index.ts +24 -3
- package/src/output-parser.ts +5 -0
- package/src/process-pump.ts +32 -1
- package/src/runtime-observations.ts +80 -0
- package/src/spawn-env.ts +3 -0
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';
|
|
@@ -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
|
-
|
|
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;
|
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;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB;IACjB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB,CAAC;AAqBF;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,mBAAmB,CAAC;IAC5B,IAAI,EAAE,iBAAiB,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,mBAAmB,CAAC;IAC5B,GAAG,CAAC,EAAE,aAAa,CAAC;CACrB,CAAC;AAEF,qBAAa,iBAAkB,YAAW,eAAe;
|
|
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];
|
|
@@ -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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|
|
@@ -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;
|
|
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"}
|
package/dist/output-parser.js
CHANGED
|
@@ -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);
|
package/dist/process-pump.d.ts
CHANGED
|
@@ -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":"AAAA,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,gBAAgB,
|
|
1
|
+
{"version":3,"file":"process-pump.d.ts","sourceRoot":"","sources":["../src/process-pump.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,gBAAgB,EAErB,KAAK,uBAAuB,EAC7B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AACrF,OAAO,KAAK,EAAE,iBAAiB,EAA0B,MAAM,oBAAoB,CAAC;AACpF,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,OAAO,KAAK,EAAE,mBAAmB,EAAgC,MAAM,gBAAgB,CAAC;AAKxF,MAAM,MAAM,sBAAsB,GAAG;IACnC,oFAAoF;IACpF,aAAa,CAAC,IAAI,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,MAAM,GAAG,SAAS,CAAC;IACxF,mEAAmE;IACnE,OAAO,IAAI,IAAI,CAAC;IAChB,yCAAyC;IACzC,KAAK,IAAI,IAAI,CAAC;IACd,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACnD,mBAAmB,IAAI,IAAI,CAAC;IAC5B,aAAa,CAAC,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAAC;CACtD,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,cAAc,CAAC,iBAAiB,CAAC,CAAC;IAC1C,MAAM,EAAE,mBAAmB,CAAC;IAC5B,MAAM,EAAE,mBAAmB,CAAC;IAC5B,KAAK,EAAE,sBAAsB,CAAC;IAC9B,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,iBAAiB;IAYhB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAXjC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,YAAY,cAAyC;IACrD,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAQ;IAC7C,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAAyB;IAC5C,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,WAAW,CAAoC;IACvD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,mBAAmB,CAAiC;gBAE/B,IAAI,EAAE,wBAAwB;IAK3D,KAAK,IAAI,IAAI;IAIb,+EAA+E;IAC/E,cAAc,CAAC,QAAQ,EAAE,mBAAmB,EAAE,YAAY,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI;IAK9E,gBAAgB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAIrD,kBAAkB,IAAI,OAAO;IAK7B;;;;;OAKG;IACH,UAAU,CAAC,GAAG,SAAa,GAAG,OAAO;IAIrC,SAAS,CAAC,GAAG,SAAa,GAAG,gBAAgB;IAU7C,sEAAsE;IACtE,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAQtC;;;OAGG;IACH,KAAK,CAAC,MAAM,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAYzD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI;IAK9B,OAAO,CAAC,SAAS;IA0BjB,gBAAgB,CAAC,MAAM,EAAE,mBAAmB,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;YAevD,GAAG;YAeH,KAAK;IAqDnB,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,gBAAgB;IAmBxB,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,eAAe;IAkCvB,OAAO,CAAC,UAAU;YAyBJ,eAAe;IA8C7B,OAAO,CAAC,UAAU;YA6BJ,aAAa;IA8C3B,OAAO,CAAC,qBAAqB;YAOf,SAAS;CA0BxB"}
|
package/dist/process-pump.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/spawn-env.d.ts.map
CHANGED
|
@@ -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,
|
|
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.
|
|
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.
|
|
29
|
-
"@parall/cli": "1.
|
|
30
|
-
"@parall/sdk": "1.
|
|
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
|
-
|
|
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.
|
|
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) =>
|
|
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
|
-
|
|
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/output-parser.ts
CHANGED
|
@@ -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
|
package/src/process-pump.ts
CHANGED
|
@@ -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,
|