@parall/claude-agent 1.59.0 → 1.60.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/busy-state.d.ts +56 -0
- package/dist/busy-state.d.ts.map +1 -0
- package/dist/busy-state.js +140 -0
- package/dist/compact.d.ts +29 -0
- package/dist/compact.d.ts.map +1 -0
- package/dist/compact.js +98 -0
- package/dist/dispatch.d.ts +52 -6
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +179 -191
- package/dist/index.js +6 -2
- package/dist/input-lifecycle.d.ts +10 -1
- package/dist/input-lifecycle.d.ts.map +1 -1
- package/dist/input-lifecycle.js +11 -2
- package/dist/output-parser.d.ts +32 -0
- package/dist/output-parser.d.ts.map +1 -1
- package/dist/output-parser.js +106 -10
- package/dist/process-pump.d.ts +105 -0
- package/dist/process-pump.d.ts.map +1 -0
- package/dist/process-pump.js +436 -0
- package/dist/runtime-turn.d.ts +19 -0
- package/dist/runtime-turn.d.ts.map +1 -0
- package/dist/runtime-turn.js +59 -0
- package/dist/session-manager.d.ts +1 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +3 -0
- package/dist/turn-sink.d.ts +49 -0
- package/dist/turn-sink.d.ts.map +1 -0
- package/dist/turn-sink.js +9 -0
- package/package.json +4 -4
- package/src/busy-state.ts +159 -0
- package/src/compact.ts +132 -0
- package/src/dispatch.ts +201 -224
- package/src/index.ts +6 -1
- package/src/input-lifecycle.ts +25 -1
- package/src/output-parser.ts +140 -10
- package/src/process-pump.ts +493 -0
- package/src/runtime-turn.ts +80 -0
- package/src/session-manager.ts +4 -0
- package/src/turn-sink.ts +59 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { RuntimeBusyState } from '@parall/agent-core';
|
|
2
|
+
import type { ClaudeRuntimeTaskEvent } from './output-parser.js';
|
|
3
|
+
export type ClaudeBackgroundTask = {
|
|
4
|
+
taskId: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
/** Ambient tasks (e.g. Monitor) never finish on their own. */
|
|
7
|
+
ambient: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type ClaudeTaskNotification = {
|
|
10
|
+
taskId?: string;
|
|
11
|
+
status?: string;
|
|
12
|
+
summary?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
at: number;
|
|
15
|
+
};
|
|
16
|
+
/** Default follow-up hold after a background task finishes (ms). */
|
|
17
|
+
export declare const DEFAULT_FOLLOW_UP_HOLD_MS = 30000;
|
|
18
|
+
/**
|
|
19
|
+
* Per-process background-work ledger fed by the CLI's task frames
|
|
20
|
+
* (`system/background_tasks_changed`, `task_started`, `task_notification`).
|
|
21
|
+
*
|
|
22
|
+
* `busy` is: a turn is running, OR a background task finished within the
|
|
23
|
+
* follow-up hold and the CLI has not yet started (or folded) the follow-up
|
|
24
|
+
* turn it usually runs on that notification. Outstanding background tasks by
|
|
25
|
+
* themselves are NOT busy — a `make dev` may run forever and must never
|
|
26
|
+
* block a restart or a shutdown drain.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ClaudeBusyTracker {
|
|
29
|
+
private readonly holdMs;
|
|
30
|
+
readonly backgroundTasks: Map<string, ClaudeBackgroundTask>;
|
|
31
|
+
followUpHoldUntil: number | null;
|
|
32
|
+
lastNotification: ClaudeTaskNotification | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Descriptions of tasks that already left the live set: the REPLACE frame
|
|
35
|
+
* arrives BEFORE the task_notification for the task it removed, and the
|
|
36
|
+
* notification itself carries only a summary.
|
|
37
|
+
*/
|
|
38
|
+
private readonly finishedDescriptions;
|
|
39
|
+
constructor(holdMs?: number);
|
|
40
|
+
onTaskFrame(frame: ClaudeRuntimeTaskEvent, now?: number): void;
|
|
41
|
+
/** A turn started (dispatch or runtime-initiated): the hold did its job. */
|
|
42
|
+
clearHold(): void;
|
|
43
|
+
holdActive(now?: number): boolean;
|
|
44
|
+
/** The hold's expiry while it is still pending, else undefined. */
|
|
45
|
+
activeHoldUntil(now?: number): number | undefined;
|
|
46
|
+
/** The notification a runtime-initiated turn most plausibly follows. */
|
|
47
|
+
takeRecentNotification(now?: number): ClaudeTaskNotification | undefined;
|
|
48
|
+
outstanding(): {
|
|
49
|
+
total: number;
|
|
50
|
+
ambient: number;
|
|
51
|
+
};
|
|
52
|
+
/** Background shells die with the process. */
|
|
53
|
+
reset(): void;
|
|
54
|
+
}
|
|
55
|
+
export declare function aggregateBusyState(states: RuntimeBusyState[]): RuntimeBusyState;
|
|
56
|
+
//# sourceMappingURL=busy-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"busy-state.d.ts","sourceRoot":"","sources":["../src/busy-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAEjE,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8DAA8D;IAC9D,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAEhD;;;;;;;;;GASG;AACH,qBAAa,iBAAiB;IAWhB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAVnC,QAAQ,CAAC,eAAe,oCAA2C;IACnE,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAQ;IACxC,gBAAgB,EAAE,sBAAsB,GAAG,SAAS,CAAC;IACrD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAA0D;gBAElE,MAAM,GAAE,MAAkC;IAEvE,WAAW,CAAC,KAAK,EAAE,sBAAsB,EAAE,GAAG,SAAa,GAAG,IAAI;IA6DlE,4EAA4E;IAC5E,SAAS,IAAI,IAAI;IAIjB,UAAU,CAAC,GAAG,SAAa,GAAG,OAAO;IAIrC,mEAAmE;IACnE,eAAe,CAAC,GAAG,SAAa,GAAG,MAAM,GAAG,SAAS;IAMrD,wEAAwE;IACxE,sBAAsB,CAAC,GAAG,SAAa,GAAG,sBAAsB,GAAG,SAAS;IAS5E,WAAW,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;IAMjD,8CAA8C;IAC9C,KAAK,IAAI,IAAI;CAMd;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,CAY/E"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/** Default follow-up hold after a background task finishes (ms). */
|
|
2
|
+
export const DEFAULT_FOLLOW_UP_HOLD_MS = 30_000;
|
|
3
|
+
/**
|
|
4
|
+
* Per-process background-work ledger fed by the CLI's task frames
|
|
5
|
+
* (`system/background_tasks_changed`, `task_started`, `task_notification`).
|
|
6
|
+
*
|
|
7
|
+
* `busy` is: a turn is running, OR a background task finished within the
|
|
8
|
+
* follow-up hold and the CLI has not yet started (or folded) the follow-up
|
|
9
|
+
* turn it usually runs on that notification. Outstanding background tasks by
|
|
10
|
+
* themselves are NOT busy — a `make dev` may run forever and must never
|
|
11
|
+
* block a restart or a shutdown drain.
|
|
12
|
+
*/
|
|
13
|
+
export class ClaudeBusyTracker {
|
|
14
|
+
holdMs;
|
|
15
|
+
backgroundTasks = new Map();
|
|
16
|
+
followUpHoldUntil = null;
|
|
17
|
+
lastNotification;
|
|
18
|
+
/**
|
|
19
|
+
* Descriptions of tasks that already left the live set: the REPLACE frame
|
|
20
|
+
* arrives BEFORE the task_notification for the task it removed, and the
|
|
21
|
+
* notification itself carries only a summary.
|
|
22
|
+
*/
|
|
23
|
+
finishedDescriptions = new Map();
|
|
24
|
+
constructor(holdMs = DEFAULT_FOLLOW_UP_HOLD_MS) {
|
|
25
|
+
this.holdMs = holdMs;
|
|
26
|
+
}
|
|
27
|
+
onTaskFrame(frame, now = Date.now()) {
|
|
28
|
+
switch (frame.subtype) {
|
|
29
|
+
case 'background_tasks_changed': {
|
|
30
|
+
// REPLACE semantics: the frame lists every live task after the change.
|
|
31
|
+
const next = new Map();
|
|
32
|
+
for (const task of frame.tasks ?? []) {
|
|
33
|
+
const prior = this.backgroundTasks.get(task.taskId);
|
|
34
|
+
next.set(task.taskId, {
|
|
35
|
+
taskId: task.taskId,
|
|
36
|
+
description: task.description ?? prior?.description,
|
|
37
|
+
ambient: task.ambient ?? prior?.ambient ?? false,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
for (const [id, task] of this.backgroundTasks) {
|
|
41
|
+
if (!next.has(id) && task.description) {
|
|
42
|
+
this.finishedDescriptions.set(id, { description: task.description, at: now });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Nothing older than takeRecentNotification's horizon is ever read
|
|
46
|
+
// again: tasks that ended without a notification must not accumulate.
|
|
47
|
+
for (const [id, entry] of this.finishedDescriptions) {
|
|
48
|
+
if (now - entry.at > this.holdMs * 2)
|
|
49
|
+
this.finishedDescriptions.delete(id);
|
|
50
|
+
}
|
|
51
|
+
this.backgroundTasks.clear();
|
|
52
|
+
for (const [id, task] of next)
|
|
53
|
+
this.backgroundTasks.set(id, task);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
case 'task_started': {
|
|
57
|
+
if (!frame.taskId || frame.isBackgrounded === false)
|
|
58
|
+
return;
|
|
59
|
+
const prior = this.backgroundTasks.get(frame.taskId);
|
|
60
|
+
this.backgroundTasks.set(frame.taskId, {
|
|
61
|
+
taskId: frame.taskId,
|
|
62
|
+
description: frame.description ?? prior?.description,
|
|
63
|
+
ambient: prior?.ambient ?? false,
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
case 'task_notification': {
|
|
68
|
+
const prior = frame.taskId ? this.backgroundTasks.get(frame.taskId) : undefined;
|
|
69
|
+
const finished = frame.taskId ? this.finishedDescriptions.get(frame.taskId) : undefined;
|
|
70
|
+
if (frame.taskId) {
|
|
71
|
+
this.backgroundTasks.delete(frame.taskId);
|
|
72
|
+
this.finishedDescriptions.delete(frame.taskId);
|
|
73
|
+
}
|
|
74
|
+
this.lastNotification = {
|
|
75
|
+
taskId: frame.taskId,
|
|
76
|
+
status: frame.status,
|
|
77
|
+
summary: frame.summary,
|
|
78
|
+
description: prior?.description ?? finished?.description ?? frame.description,
|
|
79
|
+
at: now,
|
|
80
|
+
};
|
|
81
|
+
// Unconditional: if the notification folds into a running turn the
|
|
82
|
+
// hold expires harmlessly (bounded by holdMs).
|
|
83
|
+
this.followUpHoldUntil = now + this.holdMs;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
default:
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** A turn started (dispatch or runtime-initiated): the hold did its job. */
|
|
91
|
+
clearHold() {
|
|
92
|
+
this.followUpHoldUntil = null;
|
|
93
|
+
}
|
|
94
|
+
holdActive(now = Date.now()) {
|
|
95
|
+
return this.activeHoldUntil(now) !== undefined;
|
|
96
|
+
}
|
|
97
|
+
/** The hold's expiry while it is still pending, else undefined. */
|
|
98
|
+
activeHoldUntil(now = Date.now()) {
|
|
99
|
+
return this.followUpHoldUntil !== null && this.followUpHoldUntil > now
|
|
100
|
+
? this.followUpHoldUntil
|
|
101
|
+
: undefined;
|
|
102
|
+
}
|
|
103
|
+
/** The notification a runtime-initiated turn most plausibly follows. */
|
|
104
|
+
takeRecentNotification(now = Date.now()) {
|
|
105
|
+
const notification = this.lastNotification;
|
|
106
|
+
this.lastNotification = undefined;
|
|
107
|
+
if (!notification)
|
|
108
|
+
return undefined;
|
|
109
|
+
// A notification older than two hold windows is not "recent" — the
|
|
110
|
+
// turn opening now is more likely unrelated; report it as such.
|
|
111
|
+
return now - notification.at <= this.holdMs * 2 ? notification : undefined;
|
|
112
|
+
}
|
|
113
|
+
outstanding() {
|
|
114
|
+
let ambient = 0;
|
|
115
|
+
for (const task of this.backgroundTasks.values())
|
|
116
|
+
if (task.ambient)
|
|
117
|
+
ambient += 1;
|
|
118
|
+
return { total: this.backgroundTasks.size, ambient };
|
|
119
|
+
}
|
|
120
|
+
/** Background shells die with the process. */
|
|
121
|
+
reset() {
|
|
122
|
+
this.backgroundTasks.clear();
|
|
123
|
+
this.finishedDescriptions.clear();
|
|
124
|
+
this.followUpHoldUntil = null;
|
|
125
|
+
this.lastNotification = undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export function aggregateBusyState(states) {
|
|
129
|
+
let activeTurns = 0;
|
|
130
|
+
let backgroundWork = 0;
|
|
131
|
+
let holdUntil;
|
|
132
|
+
for (const state of states) {
|
|
133
|
+
activeTurns += state.activeTurns;
|
|
134
|
+
backgroundWork += state.backgroundWork;
|
|
135
|
+
if (state.holdUntil !== undefined && (holdUntil === undefined || state.holdUntil > holdUntil)) {
|
|
136
|
+
holdUntil = state.holdUntil;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { activeTurns, backgroundWork, ...(holdUntil !== undefined ? { holdUntil } : {}) };
|
|
140
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CompactOpts, CompactResult, GatewayLogger } from '@parall/agent-core';
|
|
2
|
+
import type { ProcessState } from './dispatch.js';
|
|
3
|
+
import type { ClaudeProcessHandle } from './session-manager.js';
|
|
4
|
+
/**
|
|
5
|
+
* Idle auto-compact on the Claude bridge (idle-auto-compact-design.md §3.4):
|
|
6
|
+
* write a `/compact` user frame into the long-lived process — the Agent
|
|
7
|
+
* SDK's documented way to run the built-in — and wait for that command's
|
|
8
|
+
* lifecycle to settle. The process pump (process-pump.ts) is the only
|
|
9
|
+
* stdout reader: the command is registered like a dispatch delivery, the
|
|
10
|
+
* pump routes its frames into the delivery's sink and records the evidence
|
|
11
|
+
* on it — a `system/compact_boundary` means the CLI folded history (`done`);
|
|
12
|
+
* a result without one is the CLI declining ("Not enough messages to
|
|
13
|
+
* compact." — `noop`). The gateway holds the lane, so no dispatch shares the
|
|
14
|
+
* process while this runs. Nothing here persists steps, sends messages or
|
|
15
|
+
* touches session status.
|
|
16
|
+
*
|
|
17
|
+
* The host is the narrow slice of ClaudeCodeAdapter's process bookkeeping
|
|
18
|
+
* this needs; the adapter's `compact()` is a thin delegate.
|
|
19
|
+
*/
|
|
20
|
+
export interface ClaudeCompactHost {
|
|
21
|
+
ensureRuntimeCapability(log: GatewayLogger | undefined): Promise<void>;
|
|
22
|
+
ensureProcess(sessionKey: string, log: GatewayLogger | undefined): ProcessState;
|
|
23
|
+
killProcess(sessionKey: string, state: ProcessState): void;
|
|
24
|
+
writeUserMessage(handle: ClaudeProcessHandle, text: string, commandUuid: string): void;
|
|
25
|
+
/** Lazy restart re-check once the command settled (the adapter's own gate). */
|
|
26
|
+
applyPendingRestart(sessionKey: string, state: ProcessState): void;
|
|
27
|
+
}
|
|
28
|
+
export declare function runClaudeCompact(host: ClaudeCompactHost, { sessionKey, signal, log }: CompactOpts): Promise<CompactResult>;
|
|
29
|
+
//# sourceMappingURL=compact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compact.d.ts","sourceRoot":"","sources":["../src/compact.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,iBAAiB;IAChC,uBAAuB,CAAC,GAAG,EAAE,aAAa,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,GAAG,SAAS,GAAG,YAAY,CAAC;IAChF,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;IAC3D,gBAAgB,CAAC,MAAM,EAAE,mBAAmB,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACvF,+EAA+E;IAC/E,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;CACpE;AAED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,iBAAiB,EACvB,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,WAAW,GACvC,OAAO,CAAC,aAAa,CAAC,CA4BxB"}
|
package/dist/compact.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
export async function runClaudeCompact(host, { sessionKey, signal, log }) {
|
|
3
|
+
if (signal.aborted)
|
|
4
|
+
return { status: 'timeout' };
|
|
5
|
+
let state;
|
|
6
|
+
try {
|
|
7
|
+
await host.ensureRuntimeCapability(log);
|
|
8
|
+
state = host.ensureProcess(sessionKey, log);
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
return { status: 'failed', detail: `Claude spawn failed: ${String(err)}` };
|
|
12
|
+
}
|
|
13
|
+
// Defensive: an injection still owing bookkeeping means a dispatch is
|
|
14
|
+
// not actually settled on this session — the gateway never gets here in
|
|
15
|
+
// that state, but a compaction must not steal its frames.
|
|
16
|
+
if (state.inputs.hasPendingInjections() || state.inputs.hasUnsettledInjections()) {
|
|
17
|
+
return { status: 'failed', detail: 'injections still pending on the session' };
|
|
18
|
+
}
|
|
19
|
+
const delivery = state.inputs.register(`compact:${randomUUID()}`, undefined, false);
|
|
20
|
+
try {
|
|
21
|
+
host.writeUserMessage(state.handle, '/compact', delivery.commandUuid);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
state.inputs.remove(delivery);
|
|
25
|
+
host.killProcess(sessionKey, state);
|
|
26
|
+
return { status: 'failed', detail: `Claude stdin write failed: ${String(err)}` };
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return await consumeCompact(host, sessionKey, state, delivery, signal, log);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
state.inputs.remove(delivery);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Drain the command's sink until its lifecycle settles. The pump already
|
|
37
|
+
* did the process bookkeeping (init, lifecycle, EOF); this only reads the
|
|
38
|
+
* evidence it left on the delivery. On abort the process is SIGTERMed — the
|
|
39
|
+
* session survives via --resume on the next spawn — and the pump's `killed`
|
|
40
|
+
* envelope reports `timeout`.
|
|
41
|
+
*/
|
|
42
|
+
async function consumeCompact(host, sessionKey, state, target, signal, log) {
|
|
43
|
+
const onAbort = () => host.killProcess(sessionKey, state);
|
|
44
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
45
|
+
// The budget may have expired while the process was being spawned or the
|
|
46
|
+
// frame written; an abort that already fired never reaches the listener,
|
|
47
|
+
// and without the kill the sink drain below would never settle — and the
|
|
48
|
+
// gateway's main-lane hold would leak with it.
|
|
49
|
+
if (signal.aborted)
|
|
50
|
+
onAbort();
|
|
51
|
+
try {
|
|
52
|
+
while (true) {
|
|
53
|
+
const next = await target.sink.next();
|
|
54
|
+
if (next.done)
|
|
55
|
+
break;
|
|
56
|
+
const envelope = next.value;
|
|
57
|
+
if (envelope.kind === 'runtime')
|
|
58
|
+
continue; // the summary's own frames: progress only
|
|
59
|
+
if (envelope.kind === 'terminal')
|
|
60
|
+
break;
|
|
61
|
+
// The process is gone (eof / killed) or the pump ended this window.
|
|
62
|
+
const detail = state.handle.stderrChunks.join('').trim();
|
|
63
|
+
if (detail)
|
|
64
|
+
log?.warn?.(`subprocess stderr: ${detail}`);
|
|
65
|
+
if (signal.aborted)
|
|
66
|
+
return { status: 'timeout', detail: detail || undefined };
|
|
67
|
+
const exit = await state.handle.exitPromise.catch(() => ({ code: null, signal: null }));
|
|
68
|
+
return {
|
|
69
|
+
status: 'failed',
|
|
70
|
+
detail: detail ||
|
|
71
|
+
envelope.message ||
|
|
72
|
+
`Claude exited with code ${exit.code ?? 'unknown'}${exit.signal ? ` (${exit.signal})` : ''}`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
signal.removeEventListener('abort', onAbort);
|
|
78
|
+
}
|
|
79
|
+
host.applyPendingRestart(sessionKey, state);
|
|
80
|
+
const { compactBoundary, zeroTurnResultMeta, lastResultMeta } = target.evidence;
|
|
81
|
+
// The /compact result frame reports num_turns 0 (no model turn ran on the
|
|
82
|
+
// user's behalf): it is this command's boundary, not the resume poison
|
|
83
|
+
// frame, so it is read from the zero-turn slot first.
|
|
84
|
+
const result = zeroTurnResultMeta ?? lastResultMeta;
|
|
85
|
+
if (compactBoundary) {
|
|
86
|
+
return {
|
|
87
|
+
status: 'done',
|
|
88
|
+
...(compactBoundary.preTokens !== undefined ? { preTokens: compactBoundary.preTokens } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (result?.isError) {
|
|
92
|
+
return { status: 'failed', detail: result.resultText || 'compact result reported an error' };
|
|
93
|
+
}
|
|
94
|
+
if (target.terminal !== 'completed') {
|
|
95
|
+
return { status: 'failed', detail: `compact command ${target.terminal}` };
|
|
96
|
+
}
|
|
97
|
+
return { status: 'noop', detail: result?.resultText || 'no compact boundary emitted' };
|
|
98
|
+
}
|
package/dist/dispatch.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { CleanupForkOpts, DispatchAdapter, DispatchInputLifecycle, DispatchOpts, ForkOpts, RuntimeEvent } from '@parall/agent-core';
|
|
1
|
+
import type { CleanupForkOpts, CompactOpts, CompactResult, DispatchAdapter, DispatchInputLifecycle, DispatchOpts, ForkOpts, GatewayLogger, RuntimeActivityEvent, RuntimeBusyState, RuntimeEvent } from '@parall/agent-core';
|
|
2
2
|
import type { ClaudeAgentConfig } from './config.js';
|
|
3
|
-
import
|
|
3
|
+
import { ClaudeInputRegistry } from './input-lifecycle.js';
|
|
4
|
+
import { ClaudeProcessPump } from './process-pump.js';
|
|
5
|
+
import type { ClaudeProcessHandle, ClaudeSessionManager } from './session-manager.js';
|
|
4
6
|
import { buildSpawnEnv } from './spawn-env.js';
|
|
5
7
|
export { buildSpawnEnv };
|
|
6
8
|
type ClaudeCodeAdapterOptions = Pick<ClaudeAgentConfig, 'additionalDirs' | 'allowApiKey' | 'allowedTools' | 'appendSystemPrompt' | 'claudeBin' | 'claudeHome' | 'disallowedTools' | 'model' | 'permissionMode' | 'workspaceDir'> & {
|
|
@@ -32,14 +34,43 @@ type ClaudeCodeAdapterOptions = Pick<ClaudeAgentConfig, 'additionalDirs' | 'allo
|
|
|
32
34
|
* subprocess on its next shell command — no respawn needed.
|
|
33
35
|
*/
|
|
34
36
|
capabilityBinDir?: string;
|
|
37
|
+
/**
|
|
38
|
+
* How long the bridge stays busy after a background task finishes,
|
|
39
|
+
* waiting for the follow-up turn the CLI usually runs on that
|
|
40
|
+
* notification (busy-state.ts). Default 30 s.
|
|
41
|
+
*/
|
|
42
|
+
followUpHoldMs?: number;
|
|
43
|
+
/** Test port. */
|
|
44
|
+
now?: () => number;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Per-sessionKey long-lived process state. The process stays alive across
|
|
48
|
+
* dispatches; each dispatch writes one NDJSON user message to stdin and
|
|
49
|
+
* drains the envelopes the stdout pump routes to that delivery until its
|
|
50
|
+
* lifecycle terminal. The pump (process-pump.ts) is the ONLY stdout reader:
|
|
51
|
+
* between dispatches it keeps reading, so turns the CLI starts on its own
|
|
52
|
+
* (background-task follow-ups) are observed instead of piling up in the
|
|
53
|
+
* pipe. Gateway's `drainMainBuffer` serializes dispatch calls per
|
|
54
|
+
* sessionKey, so no two drains compete for the same delivery.
|
|
55
|
+
*/
|
|
56
|
+
export type ProcessState = {
|
|
57
|
+
handle: ClaudeProcessHandle;
|
|
58
|
+
pump: ClaudeProcessPump;
|
|
59
|
+
done: boolean;
|
|
60
|
+
needsRestart: boolean;
|
|
61
|
+
inputs: ClaudeInputRegistry;
|
|
62
|
+
log?: GatewayLogger;
|
|
35
63
|
};
|
|
36
64
|
export declare class ClaudeCodeAdapter implements DispatchAdapter {
|
|
37
65
|
private readonly opts;
|
|
38
66
|
readonly inputLifecycleMode: "explicit";
|
|
39
67
|
private readonly processes;
|
|
68
|
+
/** Retired by abortDispatch while the CLI still had its own work: busy until idle, then terminated. */
|
|
69
|
+
private readonly retiring;
|
|
40
70
|
private capabilityProbe?;
|
|
41
71
|
private capabilityProbeHandle?;
|
|
42
72
|
private shuttingDown;
|
|
73
|
+
private readonly activity;
|
|
43
74
|
private _model;
|
|
44
75
|
private _effortLevel;
|
|
45
76
|
private _contextWindow;
|
|
@@ -54,6 +85,9 @@ export declare class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
54
85
|
maxTokens?: number | null;
|
|
55
86
|
}): void;
|
|
56
87
|
requestProcessRestart(): void;
|
|
88
|
+
subscribeRuntimeActivity(handler: (event: RuntimeActivityEvent) => void): () => void;
|
|
89
|
+
busyState(now?: number): RuntimeBusyState;
|
|
90
|
+
isBusy(now?: number): boolean;
|
|
57
91
|
enqueueDuringDispatch(sessionKey: string, body: string, inputLifecycle?: DispatchInputLifecycle): boolean;
|
|
58
92
|
abortDispatch(sessionKey: string): void;
|
|
59
93
|
hasPendingInjections(sessionKey: string): boolean;
|
|
@@ -66,7 +100,10 @@ export declare class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
66
100
|
cleanupFork({ fork }: CleanupForkOpts): void;
|
|
67
101
|
resetProcesses(): void;
|
|
68
102
|
shutdown(): Promise<void>;
|
|
103
|
+
private now;
|
|
69
104
|
private runTurn;
|
|
105
|
+
/** Idle auto-compact (compact.ts): a `/compact` frame into the long-lived process. */
|
|
106
|
+
compact(opts: CompactOpts): Promise<CompactResult>;
|
|
70
107
|
private ensureRuntimeCapability;
|
|
71
108
|
/**
|
|
72
109
|
* Claude does not emit system.init until it receives its first stdin
|
|
@@ -76,15 +113,24 @@ export declare class ClaudeCodeAdapter implements DispatchAdapter {
|
|
|
76
113
|
*/
|
|
77
114
|
private probeRuntimeCapability;
|
|
78
115
|
/**
|
|
79
|
-
* Drain the
|
|
80
|
-
* reaches a terminal lifecycle state. Other injected inputs may start
|
|
81
|
-
* finish while this drain is active; their callbacks advance
|
|
82
|
-
* and their later bookkeeping dispatch becomes a no-op.
|
|
116
|
+
* Drain the envelopes the pump routed to `target` until its exact stdin
|
|
117
|
+
* UUID reaches a terminal lifecycle state. Other injected inputs may start
|
|
118
|
+
* and finish while this drain is active; their callbacks advance in the
|
|
119
|
+
* pump independently and their later bookkeeping dispatch becomes a no-op.
|
|
83
120
|
*/
|
|
84
121
|
private consumeDelivery;
|
|
122
|
+
/**
|
|
123
|
+
* Lazy restart: kill the process so the next dispatch respawns it (the
|
|
124
|
+
* session survives via --resume). Waits for every injection to settle, for
|
|
125
|
+
* an open runtime-initiated turn and for a follow-up hold — but NOT for
|
|
126
|
+
* outstanding background tasks (a `make dev` would defer a config change
|
|
127
|
+
* forever); those die with the process, logged.
|
|
128
|
+
*/
|
|
129
|
+
private maybeApplyRestart;
|
|
85
130
|
private ensureProcess;
|
|
86
131
|
private spawnProcess;
|
|
87
132
|
private killProcess;
|
|
133
|
+
private endStdin;
|
|
88
134
|
private terminateHandle;
|
|
89
135
|
private writeCapabilityProbe;
|
|
90
136
|
private writeUserMessage;
|
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,eAAe,EACf,sBAAsB,EACtB,YAAY,EACZ,QAAQ,
|
|
1
|
+
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
|