@borgee/agents-host 0.2.84 → 0.2.97
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/README.md +1 -1
- package/dist/agents-host.js +23 -5
- package/dist/background-runs.d.ts +6 -1
- package/dist/background-runs.js +56 -21
- package/dist/chat/chat-control-plane.d.ts +3 -1
- package/dist/chat/sdk-chat-control-plane.d.ts +6 -2
- package/dist/chat/sdk-chat-control-plane.js +18 -0
- package/dist/plugin-sdk.js +149 -21
- package/dist/plugin-sdk.js.map +3 -3
- package/dist/progress-to-activity.d.ts +3 -3
- package/dist/providers/claude/adapter.d.ts +3 -1
- package/dist/providers/claude/adapter.js +41 -3
- package/dist/providers/claude/background-run-observer.d.ts +32 -0
- package/dist/providers/claude/background-run-observer.js +381 -0
- package/dist/providers/claude/cli-client.d.ts +15 -1
- package/dist/providers/claude/cli-client.js +699 -69
- package/dist/providers/claude/foreground-handoff.d.ts +4 -0
- package/dist/providers/claude/foreground-handoff.js +45 -0
- package/dist/providers/claude/task-cancellation-protocol.d.ts +14 -0
- package/dist/providers/claude/task-cancellation-protocol.js +21 -0
- package/dist/providers/copilot/activity-metadata.d.ts +3 -0
- package/dist/providers/copilot/activity-metadata.js +19 -0
- package/dist/providers/copilot/cli-client.js +5 -0
- package/dist/providers/provider-adapter.d.ts +8 -0
- package/dist/providers/provider-adapter.js +4 -0
- package/dist/types.d.ts +8 -0
- package/dist/vendor/claude-agent-acp/LICENSE +191 -0
- package/dist/vendor/claude-agent-acp/NOTICE +8 -0
- package/dist/vendor/claude-agent-acp/dist/acp-agent.d.ts +1017 -0
- package/dist/vendor/claude-agent-acp/dist/acp-agent.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/acp-agent.js +6305 -0
- package/dist/vendor/claude-agent-acp/dist/borgee-foreground-handoff-bridge.js +322 -0
- package/dist/vendor/claude-agent-acp/dist/borgee-task-cancellation-bridge.js +101 -0
- package/dist/vendor/claude-agent-acp/dist/borgee-task-lifecycle-bridge.js +58 -0
- package/dist/vendor/claude-agent-acp/dist/elicitation.d.ts +130 -0
- package/dist/vendor/claude-agent-acp/dist/elicitation.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/elicitation.js +304 -0
- package/dist/vendor/claude-agent-acp/dist/index.d.ts +3 -0
- package/dist/vendor/claude-agent-acp/dist/index.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/index.js +75 -0
- package/dist/vendor/claude-agent-acp/dist/lib.d.ts +6 -0
- package/dist/vendor/claude-agent-acp/dist/lib.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/lib.js +5 -0
- package/dist/vendor/claude-agent-acp/dist/settings.d.ts +68 -0
- package/dist/vendor/claude-agent-acp/dist/settings.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/settings.js +185 -0
- package/dist/vendor/claude-agent-acp/dist/tools.d.ts +102 -0
- package/dist/vendor/claude-agent-acp/dist/tools.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/tools.js +1000 -0
- package/dist/vendor/claude-agent-acp/dist/utils.d.ts +16 -0
- package/dist/vendor/claude-agent-acp/dist/utils.d.ts.map +1 -0
- package/dist/vendor/claude-agent-acp/dist/utils.js +81 -0
- package/dist/vendor/claude-agent-acp/package.json +85 -0
- package/package.json +16 -12
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Readable, Writable } from 'node:stream';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { createRequire } from 'node:module';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
4
|
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import spawn from 'cross-spawn';
|
|
7
7
|
import { DEFAULT_PROJECTION_STRATEGY, normalizeProjectionStrategy, } from '../../projection-strategy-values.js';
|
|
8
8
|
import { assertClaudeCommandCompatibility, DEFAULT_CLAUDE_COMMAND, isLegacyClaudeCompatibilityAlias, } from '../../config.js';
|
|
@@ -11,10 +11,347 @@ import { HostLogger, summarizeChildStderr, summarizeError } from '../../debug.js
|
|
|
11
11
|
import { AcpProgressCollector } from '../acp-progress-collector.js';
|
|
12
12
|
import { ProviderTurnCancelledError } from '../provider-adapter.js';
|
|
13
13
|
import { readClaudeActivityMetadata } from './activity-metadata.js';
|
|
14
|
+
import { ClaudeBackgroundRunObserver } from './background-run-observer.js';
|
|
15
|
+
import { CLAUDE_FOREGROUND_HANDOFF_METHOD, isClaudeUpdateFromIsolatedTurn, readClaudeForegroundSettledTurn, } from './foreground-handoff.js';
|
|
14
16
|
import { resolveCopilotPermissionResponse } from '../../policy/copilot-permission.js';
|
|
15
17
|
import { buildClaudeSessionPromptAppend } from '../../context/prompt.js';
|
|
16
18
|
import { isDiscussionOnlyResolvedWorkingFolder } from '../../context/resolved-working-folder.js';
|
|
17
19
|
import { IDLE_BACKEND_SHUTDOWN_DISABLED_MS, IdleBackendShutdownScheduler, } from '../idle-backend-shutdown.js';
|
|
20
|
+
import { CLAUDE_TASK_CANCELLATION_METHOD, supportsClaudeTaskCancellation, } from './task-cancellation-protocol.js';
|
|
21
|
+
const BACKGROUND_COMPLETION_SIGNAL_TTL_MS = 30_000;
|
|
22
|
+
const MAX_BACKGROUND_COMPLETION_SIGNALS = 32;
|
|
23
|
+
const AUTONOMOUS_REPLY_IDLE_FLUSH_MS = 100;
|
|
24
|
+
class ClaudeSessionUpdatePump {
|
|
25
|
+
session;
|
|
26
|
+
onFailure;
|
|
27
|
+
autonomousEvents;
|
|
28
|
+
onForegroundSettledWithBackground;
|
|
29
|
+
observer;
|
|
30
|
+
activeTurn;
|
|
31
|
+
autonomousTurns = new Map();
|
|
32
|
+
backgroundCompletionSignals = [];
|
|
33
|
+
backgroundCompletionSignalExpiry;
|
|
34
|
+
autonomousTurnPending = false;
|
|
35
|
+
closed = false;
|
|
36
|
+
failure;
|
|
37
|
+
constructor(session, onFailure, onBackgroundTasksActiveChanged, autonomousEvents, onForegroundSettledWithBackground, targetedCancellationSupported) {
|
|
38
|
+
this.session = session;
|
|
39
|
+
this.onFailure = onFailure;
|
|
40
|
+
this.autonomousEvents = autonomousEvents;
|
|
41
|
+
this.onForegroundSettledWithBackground = onForegroundSettledWithBackground;
|
|
42
|
+
this.observer = new ClaudeBackgroundRunObserver(undefined, {
|
|
43
|
+
onBackgroundTasksActiveChanged,
|
|
44
|
+
targetedCancellationSupported,
|
|
45
|
+
});
|
|
46
|
+
void this.run();
|
|
47
|
+
}
|
|
48
|
+
beginTurn(onProgress, foregroundIsolationFence) {
|
|
49
|
+
if (this.closed) {
|
|
50
|
+
throw new Error('Claude ACP session update pump is closed');
|
|
51
|
+
}
|
|
52
|
+
if (this.failure) {
|
|
53
|
+
throw this.failure;
|
|
54
|
+
}
|
|
55
|
+
if (this.activeTurn) {
|
|
56
|
+
throw new Error('Claude ACP session already has an active turn');
|
|
57
|
+
}
|
|
58
|
+
const turn = {
|
|
59
|
+
queue: [],
|
|
60
|
+
foregroundIsolationFence,
|
|
61
|
+
ownedForegroundTurn: this.resolveForegroundTurn(foregroundIsolationFence),
|
|
62
|
+
};
|
|
63
|
+
this.activeTurn = turn;
|
|
64
|
+
this.observer.setOnProgress(onProgress);
|
|
65
|
+
return {
|
|
66
|
+
next: () => {
|
|
67
|
+
if (turn.failure) {
|
|
68
|
+
return Promise.reject(turn.failure);
|
|
69
|
+
}
|
|
70
|
+
const queued = turn.queue.shift();
|
|
71
|
+
if (queued) {
|
|
72
|
+
return Promise.resolve(queued);
|
|
73
|
+
}
|
|
74
|
+
if (this.activeTurn !== turn || this.closed) {
|
|
75
|
+
return Promise.reject(new Error('Claude ACP turn update stream is closed'));
|
|
76
|
+
}
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
turn.waiter = { resolve, reject };
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
close: () => {
|
|
82
|
+
if (this.activeTurn !== turn) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.activeTurn = undefined;
|
|
86
|
+
turn.waiter?.reject(new Error('Claude ACP turn update stream is closed'));
|
|
87
|
+
turn.waiter = undefined;
|
|
88
|
+
turn.queue.length = 0;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
close() {
|
|
93
|
+
if (this.closed) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
this.closed = true;
|
|
97
|
+
this.observer.dispose();
|
|
98
|
+
if (this.backgroundCompletionSignalExpiry) {
|
|
99
|
+
clearTimeout(this.backgroundCompletionSignalExpiry);
|
|
100
|
+
this.backgroundCompletionSignalExpiry = undefined;
|
|
101
|
+
}
|
|
102
|
+
this.backgroundCompletionSignals.length = 0;
|
|
103
|
+
for (const turn of this.autonomousTurns.values()) {
|
|
104
|
+
if (turn.flushTimer) {
|
|
105
|
+
clearTimeout(turn.flushTimer);
|
|
106
|
+
}
|
|
107
|
+
if (turn.discardTimer) {
|
|
108
|
+
clearTimeout(turn.discardTimer);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
this.autonomousTurns.clear();
|
|
112
|
+
this.refreshAutonomousTurnPresence();
|
|
113
|
+
const turn = this.activeTurn;
|
|
114
|
+
this.activeTurn = undefined;
|
|
115
|
+
turn?.waiter?.reject(new Error('Claude ACP session update stream is closed'));
|
|
116
|
+
}
|
|
117
|
+
resolveForegroundTurn(foregroundIsolationFence) {
|
|
118
|
+
const nextForegroundTurn = foregroundIsolationFence === undefined ? 1 : foregroundIsolationFence + 1;
|
|
119
|
+
return Number.isSafeInteger(nextForegroundTurn) && nextForegroundTurn > 0
|
|
120
|
+
? nextForegroundTurn
|
|
121
|
+
: undefined;
|
|
122
|
+
}
|
|
123
|
+
autonomousTurnFor(foregroundTurn) {
|
|
124
|
+
let turn = this.autonomousTurns.get(foregroundTurn);
|
|
125
|
+
if (turn) {
|
|
126
|
+
return turn;
|
|
127
|
+
}
|
|
128
|
+
turn = {
|
|
129
|
+
eventId: `claude-background-${foregroundTurn}`,
|
|
130
|
+
confirmed: false,
|
|
131
|
+
text: '',
|
|
132
|
+
};
|
|
133
|
+
this.autonomousTurns.set(foregroundTurn, turn);
|
|
134
|
+
return turn;
|
|
135
|
+
}
|
|
136
|
+
noteBackgroundCompletionSignal(foregroundTurn, eventId) {
|
|
137
|
+
const turn = this.autonomousTurnFor(foregroundTurn);
|
|
138
|
+
if (turn.discardTimer) {
|
|
139
|
+
clearTimeout(turn.discardTimer);
|
|
140
|
+
turn.discardTimer = undefined;
|
|
141
|
+
}
|
|
142
|
+
this.pruneBackgroundCompletionSignals();
|
|
143
|
+
const existingIndex = this.backgroundCompletionSignals.findIndex((signal) => signal.foregroundTurn === foregroundTurn);
|
|
144
|
+
if (existingIndex >= 0) {
|
|
145
|
+
this.backgroundCompletionSignals.splice(existingIndex, 1);
|
|
146
|
+
}
|
|
147
|
+
this.backgroundCompletionSignals.push({
|
|
148
|
+
eventId,
|
|
149
|
+
foregroundTurn,
|
|
150
|
+
expiresAt: Date.now() + BACKGROUND_COMPLETION_SIGNAL_TTL_MS,
|
|
151
|
+
});
|
|
152
|
+
if (this.backgroundCompletionSignals.length > MAX_BACKGROUND_COMPLETION_SIGNALS) {
|
|
153
|
+
this.backgroundCompletionSignals.splice(0, this.backgroundCompletionSignals.length - MAX_BACKGROUND_COMPLETION_SIGNALS);
|
|
154
|
+
}
|
|
155
|
+
this.scheduleBackgroundCompletionSignalExpiry();
|
|
156
|
+
turn.confirmed = true;
|
|
157
|
+
turn.eventId = eventId;
|
|
158
|
+
if (turn.text.trim()) {
|
|
159
|
+
this.scheduleAutonomousReplyFlush(foregroundTurn, turn);
|
|
160
|
+
}
|
|
161
|
+
this.refreshAutonomousTurnPresence();
|
|
162
|
+
return turn;
|
|
163
|
+
}
|
|
164
|
+
consumeAutonomousLifecycle(update) {
|
|
165
|
+
if (update.update.sessionUpdate !== 'session_info_update') {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const provider = asObject(update.update._meta)?.claudeCode;
|
|
169
|
+
const lifecycle = asObject(asObject(provider)?.taskLifecycle);
|
|
170
|
+
if (lifecycle?.event !== 'notification' || lifecycle?.status !== 'completed') {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const foregroundTurn = asObject(provider)?.foregroundTurn;
|
|
174
|
+
if (!Number.isSafeInteger(foregroundTurn) || foregroundTurn <= 0) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const taskId = typeof lifecycle.taskId === 'string' ? lifecycle.taskId : 'task';
|
|
178
|
+
this.noteBackgroundCompletionSignal(foregroundTurn, `${taskId}:completed`);
|
|
179
|
+
}
|
|
180
|
+
consumeAutonomousUpdate(update) {
|
|
181
|
+
if (update.update.sessionUpdate !== 'agent_message_chunk' || update.update.content.type !== 'text') {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const provider = asObject(update.update._meta)?.claudeCode;
|
|
185
|
+
const foregroundTurn = asObject(provider)?.foregroundTurn;
|
|
186
|
+
if (!Number.isSafeInteger(foregroundTurn) || foregroundTurn <= 0) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const lane = asObject(provider)?.foregroundLane;
|
|
190
|
+
const activeForegroundTurn = this.activeTurn?.ownedForegroundTurn;
|
|
191
|
+
if (foregroundTurn === activeForegroundTurn) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (lane !== 'background' && !this.backgroundCompletionSignals.some((signal) => signal.foregroundTurn === foregroundTurn)) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const turn = this.autonomousTurnFor(foregroundTurn);
|
|
198
|
+
turn.text += update.update.content.text;
|
|
199
|
+
if (turn.confirmed) {
|
|
200
|
+
this.scheduleAutonomousReplyFlush(foregroundTurn, turn);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
scheduleAutonomousReplyFlush(foregroundTurn, turn) {
|
|
204
|
+
if (turn.flushTimer) {
|
|
205
|
+
clearTimeout(turn.flushTimer);
|
|
206
|
+
}
|
|
207
|
+
turn.flushTimer = setTimeout(() => {
|
|
208
|
+
turn.flushTimer = undefined;
|
|
209
|
+
this.flushAutonomousReply(foregroundTurn, turn);
|
|
210
|
+
}, AUTONOMOUS_REPLY_IDLE_FLUSH_MS);
|
|
211
|
+
turn.flushTimer.unref?.();
|
|
212
|
+
}
|
|
213
|
+
flushAutonomousReply(foregroundTurn, turn) {
|
|
214
|
+
const text = turn.text.trim();
|
|
215
|
+
if (turn.confirmed && text) {
|
|
216
|
+
this.autonomousEvents.onReply?.({
|
|
217
|
+
eventId: turn.eventId,
|
|
218
|
+
text,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
else if (turn.confirmed) {
|
|
222
|
+
turn.discardTimer = setTimeout(() => {
|
|
223
|
+
turn.discardTimer = undefined;
|
|
224
|
+
this.discardAutonomousTurn(foregroundTurn, turn);
|
|
225
|
+
}, BACKGROUND_COMPLETION_SIGNAL_TTL_MS);
|
|
226
|
+
turn.discardTimer.unref?.();
|
|
227
|
+
this.refreshAutonomousTurnPresence();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
this.discardAutonomousTurn(foregroundTurn, turn);
|
|
231
|
+
}
|
|
232
|
+
discardAutonomousTurn(foregroundTurn, turn) {
|
|
233
|
+
if (turn.flushTimer) {
|
|
234
|
+
clearTimeout(turn.flushTimer);
|
|
235
|
+
turn.flushTimer = undefined;
|
|
236
|
+
}
|
|
237
|
+
if (turn.discardTimer) {
|
|
238
|
+
clearTimeout(turn.discardTimer);
|
|
239
|
+
turn.discardTimer = undefined;
|
|
240
|
+
}
|
|
241
|
+
if (this.autonomousTurns.get(foregroundTurn) === turn) {
|
|
242
|
+
this.autonomousTurns.delete(foregroundTurn);
|
|
243
|
+
}
|
|
244
|
+
const signalIndex = this.backgroundCompletionSignals.findIndex((signal) => signal.foregroundTurn === foregroundTurn);
|
|
245
|
+
if (signalIndex >= 0) {
|
|
246
|
+
this.backgroundCompletionSignals.splice(signalIndex, 1);
|
|
247
|
+
this.scheduleBackgroundCompletionSignalExpiry();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
this.refreshAutonomousTurnPresence();
|
|
251
|
+
}
|
|
252
|
+
pruneBackgroundCompletionSignals() {
|
|
253
|
+
const now = Date.now();
|
|
254
|
+
while (this.backgroundCompletionSignals.length > 0
|
|
255
|
+
&& this.backgroundCompletionSignals[0].expiresAt <= now) {
|
|
256
|
+
const expired = this.backgroundCompletionSignals.shift();
|
|
257
|
+
if (!expired) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const turn = this.autonomousTurns.get(expired.foregroundTurn);
|
|
261
|
+
if (turn && !turn.text.trim()) {
|
|
262
|
+
this.discardAutonomousTurn(expired.foregroundTurn, turn);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
scheduleBackgroundCompletionSignalExpiry() {
|
|
267
|
+
if (this.backgroundCompletionSignalExpiry) {
|
|
268
|
+
clearTimeout(this.backgroundCompletionSignalExpiry);
|
|
269
|
+
this.backgroundCompletionSignalExpiry = undefined;
|
|
270
|
+
}
|
|
271
|
+
this.pruneBackgroundCompletionSignals();
|
|
272
|
+
const next = this.backgroundCompletionSignals[0];
|
|
273
|
+
if (!next) {
|
|
274
|
+
this.refreshAutonomousTurnPresence();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
this.backgroundCompletionSignalExpiry = setTimeout(() => {
|
|
278
|
+
this.backgroundCompletionSignalExpiry = undefined;
|
|
279
|
+
this.pruneBackgroundCompletionSignals();
|
|
280
|
+
this.scheduleBackgroundCompletionSignalExpiry();
|
|
281
|
+
}, Math.max(0, next.expiresAt - Date.now()));
|
|
282
|
+
this.backgroundCompletionSignalExpiry.unref?.();
|
|
283
|
+
}
|
|
284
|
+
refreshAutonomousTurnPresence() {
|
|
285
|
+
const pending = this.autonomousTurns.size > 0 || this.backgroundCompletionSignals.length > 0;
|
|
286
|
+
if (pending === this.autonomousTurnPending) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.autonomousTurnPending = pending;
|
|
290
|
+
this.autonomousEvents.onPendingChanged?.(pending);
|
|
291
|
+
}
|
|
292
|
+
async run() {
|
|
293
|
+
try {
|
|
294
|
+
while (!this.closed) {
|
|
295
|
+
const update = await this.session.nextUpdate();
|
|
296
|
+
if (this.closed) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const lifecycle = update.kind === 'session_update' && this.observer.consume(update.update);
|
|
300
|
+
if (lifecycle) {
|
|
301
|
+
this.consumeAutonomousLifecycle(update);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (update.kind === 'session_update') {
|
|
305
|
+
this.consumeAutonomousUpdate(update);
|
|
306
|
+
}
|
|
307
|
+
if (update.kind === 'session_update'
|
|
308
|
+
&& isClaudeUpdateFromIsolatedTurn(update.update, this.activeTurn?.foregroundIsolationFence)) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const settledForegroundTurn = update.kind === 'session_update'
|
|
312
|
+
? readClaudeForegroundSettledTurn(update.update)
|
|
313
|
+
: undefined;
|
|
314
|
+
const turn = this.activeTurn;
|
|
315
|
+
if (turn
|
|
316
|
+
&& settledForegroundTurn !== undefined
|
|
317
|
+
&& turn.foregroundSettledTurn !== settledForegroundTurn) {
|
|
318
|
+
turn.foregroundSettledTurn = settledForegroundTurn;
|
|
319
|
+
this.onForegroundSettledWithBackground(settledForegroundTurn);
|
|
320
|
+
}
|
|
321
|
+
if (!turn) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const waiter = turn.waiter;
|
|
325
|
+
if (waiter) {
|
|
326
|
+
turn.waiter = undefined;
|
|
327
|
+
waiter.resolve(update);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
turn.queue.push(update);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
if (!this.closed) {
|
|
336
|
+
const normalized = normalizeError(error);
|
|
337
|
+
this.observer.fail();
|
|
338
|
+
this.failure = normalized;
|
|
339
|
+
const turn = this.activeTurn;
|
|
340
|
+
if (turn) {
|
|
341
|
+
// ActiveSession mirrors prompt failures onto its update queue. Keep
|
|
342
|
+
// that rejection scoped to this turn/session; transport failures use
|
|
343
|
+
// the connection and child-process fatal handlers instead.
|
|
344
|
+
turn.failure = normalized;
|
|
345
|
+
turn.waiter?.reject(normalized);
|
|
346
|
+
turn.waiter = undefined;
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
this.onFailure(normalized);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
18
355
|
const SESSION_TAINTED_ERRORS = new WeakSet();
|
|
19
356
|
const IDLE_BACKEND_STOPPED_MESSAGE = 'Claude ACP backend stopped after idle timeout';
|
|
20
357
|
/**
|
|
@@ -32,7 +369,6 @@ const DEFAULT_SESSION_CAPABILITIES = {
|
|
|
32
369
|
resumeSession: false,
|
|
33
370
|
};
|
|
34
371
|
const CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD_ENV = 'CLAUDE_SDK_COMPACTION_EXPERIMENT_CONTEXT_TOKEN_THRESHOLD';
|
|
35
|
-
const require = createRequire(import.meta.url);
|
|
36
372
|
const CLAUDE_CANCELLED_REPLY_TEXT = 'Info: Operation cancelled by user';
|
|
37
373
|
const DEFAULT_RUNTIME = {
|
|
38
374
|
spawn,
|
|
@@ -283,7 +619,12 @@ function isSessionTainted(error) {
|
|
|
283
619
|
return error instanceof Error && SESSION_TAINTED_ERRORS.has(error);
|
|
284
620
|
}
|
|
285
621
|
function resolveBundledClaudeAdapterPath() {
|
|
286
|
-
|
|
622
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
623
|
+
const adapterPath = resolve(packageRoot, 'dist', 'vendor', 'claude-agent-acp', 'dist', 'index.js');
|
|
624
|
+
if (!existsSync(adapterPath)) {
|
|
625
|
+
throw new Error(`Shipped Claude ACP adapter is missing at ${adapterPath}; reinstall @borgee/agents-host`);
|
|
626
|
+
}
|
|
627
|
+
return adapterPath;
|
|
287
628
|
}
|
|
288
629
|
function isRelativePathLike(value) {
|
|
289
630
|
return value.startsWith('./') || value.startsWith('../');
|
|
@@ -489,16 +830,45 @@ export class ClaudeCliClient {
|
|
|
489
830
|
imageInputConfig;
|
|
490
831
|
runtime;
|
|
491
832
|
channels = new Map();
|
|
833
|
+
channelWorkerRoutes = new Map();
|
|
834
|
+
targetedBackgroundRunCancellationConfigured;
|
|
492
835
|
/**
|
|
493
836
|
* The provider session currently bound to channelId, or undefined before a
|
|
494
837
|
* turn has opened one. Surfaced so agents-host can record which session
|
|
495
838
|
* worked a task without the agent being asked to report its own id.
|
|
496
839
|
*/
|
|
497
840
|
sessionIdForChannel(channelId) {
|
|
498
|
-
return this.
|
|
841
|
+
return this.channelWorkerRoutes.get(channelId)?.state.session?.sessionId
|
|
842
|
+
?? this.channels.get(channelId)?.session?.sessionId;
|
|
843
|
+
}
|
|
844
|
+
async handoffForegroundTurn(channelId) {
|
|
845
|
+
const state = this.channelWorkerRoutes.get(channelId)?.state ?? this.channels.get(channelId);
|
|
846
|
+
const session = state?.session;
|
|
847
|
+
if (!state?.activeTurn || !session || !this.connection) {
|
|
848
|
+
return { outcome: 'not_found' };
|
|
849
|
+
}
|
|
850
|
+
const response = await this.connection.agent.request(CLAUDE_FOREGROUND_HANDOFF_METHOD, { sessionId: session.sessionId });
|
|
851
|
+
const result = asObject(response);
|
|
852
|
+
const outcome = result?.outcome;
|
|
853
|
+
if (outcome === 'admitted' || outcome === 'released') {
|
|
854
|
+
const foregroundTurn = result?.foregroundTurn;
|
|
855
|
+
if (!Number.isSafeInteger(foregroundTurn)
|
|
856
|
+
|| foregroundTurn <= 0) {
|
|
857
|
+
throw new Error('Claude ACP foreground handoff omitted its ownership fence');
|
|
858
|
+
}
|
|
859
|
+
if (state.session !== session) {
|
|
860
|
+
return { outcome: 'admitted' };
|
|
861
|
+
}
|
|
862
|
+
state.foregroundIsolationFence = Math.max(state.foregroundIsolationFence ?? 0, foregroundTurn);
|
|
863
|
+
return { outcome: 'admitted' };
|
|
864
|
+
}
|
|
865
|
+
if (outcome === 'not_found') {
|
|
866
|
+
return { outcome: 'not_found' };
|
|
867
|
+
}
|
|
868
|
+
throw new Error('Claude ACP returned an unusable foreground handoff result');
|
|
499
869
|
}
|
|
500
870
|
async cancelTurn(channelId) {
|
|
501
|
-
const state = this.channels.get(channelId);
|
|
871
|
+
const state = this.channelWorkerRoutes.get(channelId)?.state ?? this.channels.get(channelId);
|
|
502
872
|
const session = state?.session;
|
|
503
873
|
if (!state?.activeTurn || !session) {
|
|
504
874
|
return false;
|
|
@@ -513,8 +883,33 @@ export class ClaudeCliClient {
|
|
|
513
883
|
await this.connection?.agent.notify(cancelMethod, { sessionId: session.sessionId });
|
|
514
884
|
return true;
|
|
515
885
|
}
|
|
886
|
+
async cancelBackgroundRun(channelId, providerRunId) {
|
|
887
|
+
if (!this.targetedBackgroundRunCancellationConfigured
|
|
888
|
+
|| !this.targetedBackgroundRunCancellationAvailable
|
|
889
|
+
|| providerRunId.length === 0) {
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
892
|
+
const state = this.channelWorkerRoutes.get(channelId)?.state ?? this.channels.get(channelId);
|
|
893
|
+
const session = state?.session;
|
|
894
|
+
if (!state || state.channelId !== channelId || !session || !this.connection) {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
this.idleBackendShutdown.cancel();
|
|
898
|
+
const response = await this.connection.agent.request(CLAUDE_TASK_CANCELLATION_METHOD, {
|
|
899
|
+
sessionId: session.sessionId,
|
|
900
|
+
taskId: providerRunId,
|
|
901
|
+
});
|
|
902
|
+
if (response?.outcome === 'accepted') {
|
|
903
|
+
return true;
|
|
904
|
+
}
|
|
905
|
+
if (response?.outcome === 'not_found' || response?.outcome === 'unsupported') {
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
908
|
+
throw new Error('Claude ACP returned an invalid targeted task cancellation response');
|
|
909
|
+
}
|
|
516
910
|
persistedSessions = new Map();
|
|
517
911
|
closingSessions = new WeakSet();
|
|
912
|
+
sessionUpdatePumps = new WeakMap();
|
|
518
913
|
pendingSessionStarts = new Set();
|
|
519
914
|
pendingSessionCloses = new Set();
|
|
520
915
|
pendingSessionStoreOperations = new Set();
|
|
@@ -539,6 +934,8 @@ export class ClaudeCliClient {
|
|
|
539
934
|
sessionStoreLoadPromise = null;
|
|
540
935
|
sessionStoreWriteQueue = Promise.resolve();
|
|
541
936
|
sessionCapabilities = DEFAULT_SESSION_CAPABILITIES;
|
|
937
|
+
targetedBackgroundRunCancellationAvailable = false;
|
|
938
|
+
autonomousReplyHandler;
|
|
542
939
|
idleBackendShutdown;
|
|
543
940
|
childStderr = '';
|
|
544
941
|
constructor(command, args = [], runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined, logger = new HostLogger(), imageInputConfig = {}) {
|
|
@@ -550,6 +947,8 @@ export class ClaudeCliClient {
|
|
|
550
947
|
this.imageInputConfig = imageInputConfig;
|
|
551
948
|
this.runtime = { ...DEFAULT_RUNTIME, ...runtimeOverrides };
|
|
552
949
|
assertClaudeCommandCompatibility(command, args, 'Claude provider runtime');
|
|
950
|
+
this.targetedBackgroundRunCancellationConfigured =
|
|
951
|
+
useBundledClaudeAdapter(command, args) || useLegacyClaudeCompatibilityAlias(command, args);
|
|
553
952
|
this.idleBackendShutdown = new IdleBackendShutdownScheduler({
|
|
554
953
|
idleShutdownMs: this.runtime.idleBackendShutdownMs,
|
|
555
954
|
isIdle: () => this.isBackendIdle(),
|
|
@@ -565,6 +964,9 @@ export class ClaudeCliClient {
|
|
|
565
964
|
});
|
|
566
965
|
void this.fatalPromise.catch(() => { });
|
|
567
966
|
}
|
|
967
|
+
onAutonomousReply(handler) {
|
|
968
|
+
this.autonomousReplyHandler = handler;
|
|
969
|
+
}
|
|
568
970
|
async generateReply(channelIdOrTurn, promptOrOptions, maybeOptions) {
|
|
569
971
|
if (this.fatalError) {
|
|
570
972
|
throw this.fatalError;
|
|
@@ -586,16 +988,17 @@ export class ClaudeCliClient {
|
|
|
586
988
|
? maybeOptions
|
|
587
989
|
: promptOrOptions;
|
|
588
990
|
const sessionRouting = resolveSessionRouting(preparedTurn);
|
|
589
|
-
const state = this.
|
|
991
|
+
const state = this.resolveChannelState(sessionRouting);
|
|
590
992
|
this.idleBackendShutdown.cancel();
|
|
591
993
|
const turn = createDeferredTurn(preparedTurn.channelId, preparedTurn, sessionRouting.persistence, options);
|
|
592
994
|
state.queue.push(turn);
|
|
593
|
-
this.processChannelQueue(
|
|
995
|
+
this.processChannelQueue(state);
|
|
594
996
|
return turn.promise;
|
|
595
997
|
}
|
|
596
998
|
async dispose() {
|
|
597
999
|
const error = new Error('Claude ACP backend stopped');
|
|
598
1000
|
this.disposing = true;
|
|
1001
|
+
this.autonomousReplyHandler = undefined;
|
|
599
1002
|
this.idleBackendShutdown.cancel();
|
|
600
1003
|
this.logger?.debug('stopping Claude ACP backend');
|
|
601
1004
|
const closed = this.connection?.closed ?? Promise.resolve();
|
|
@@ -717,9 +1120,13 @@ export class ClaudeCliClient {
|
|
|
717
1120
|
},
|
|
718
1121
|
});
|
|
719
1122
|
this.sessionCapabilities = readSessionCapabilities(initializeResponse);
|
|
1123
|
+
this.targetedBackgroundRunCancellationAvailable =
|
|
1124
|
+
this.targetedBackgroundRunCancellationConfigured
|
|
1125
|
+
&& supportsClaudeTaskCancellation(initializeResponse);
|
|
720
1126
|
this.logger?.debug('initialized Claude ACP connection', {
|
|
721
1127
|
loadSession: this.sessionCapabilities.loadSession,
|
|
722
1128
|
resumeSession: this.sessionCapabilities.resumeSession,
|
|
1129
|
+
targetedBackgroundRunCancellation: this.targetedBackgroundRunCancellationAvailable,
|
|
723
1130
|
});
|
|
724
1131
|
}
|
|
725
1132
|
catch (error) {
|
|
@@ -729,24 +1136,46 @@ export class ClaudeCliClient {
|
|
|
729
1136
|
throw normalized;
|
|
730
1137
|
}
|
|
731
1138
|
}
|
|
732
|
-
getOrCreateChannelState(
|
|
733
|
-
let state = this.channels.get(
|
|
1139
|
+
getOrCreateChannelState(routingKey, resolvedChannelId, sessionPersistence) {
|
|
1140
|
+
let state = this.channels.get(routingKey);
|
|
734
1141
|
if (!state) {
|
|
735
1142
|
state = {
|
|
1143
|
+
routingKey,
|
|
736
1144
|
channelId: resolvedChannelId,
|
|
737
1145
|
sessionPersistence,
|
|
1146
|
+
backgroundTasksActive: false,
|
|
1147
|
+
backgroundContinuationPending: false,
|
|
1148
|
+
sessionHasPendingAutonomousTurn: false,
|
|
1149
|
+
persistentFollowUpAdopted: false,
|
|
738
1150
|
processing: false,
|
|
739
1151
|
queue: [],
|
|
740
1152
|
};
|
|
741
|
-
this.channels.set(
|
|
1153
|
+
this.channels.set(routingKey, state);
|
|
742
1154
|
}
|
|
743
1155
|
else if (state.channelId !== resolvedChannelId
|
|
744
1156
|
|| state.sessionPersistence !== sessionPersistence) {
|
|
745
|
-
throw new Error(`Claude session routing changed for key "${
|
|
1157
|
+
throw new Error(`Claude session routing changed for key "${routingKey}"`);
|
|
746
1158
|
}
|
|
747
1159
|
return state;
|
|
748
1160
|
}
|
|
749
|
-
|
|
1161
|
+
resolveChannelState(routing) {
|
|
1162
|
+
const workerRoute = this.channelWorkerRoutes.get(routing.channelId);
|
|
1163
|
+
if (workerRoute) {
|
|
1164
|
+
return workerRoute.state;
|
|
1165
|
+
}
|
|
1166
|
+
if (routing.persistence === 'ephemeral') {
|
|
1167
|
+
const foregroundState = this.channels.get(routing.channelId);
|
|
1168
|
+
const state = foregroundState
|
|
1169
|
+
&& foregroundState.channelId === routing.channelId
|
|
1170
|
+
&& foregroundState.sessionPersistence === 'persistent'
|
|
1171
|
+
? foregroundState
|
|
1172
|
+
: this.getOrCreateChannelState(routing.key, routing.channelId, routing.persistence);
|
|
1173
|
+
this.channelWorkerRoutes.set(routing.channelId, { phase: 'candidate', state });
|
|
1174
|
+
return state;
|
|
1175
|
+
}
|
|
1176
|
+
return this.getOrCreateChannelState(routing.key, routing.channelId, routing.persistence);
|
|
1177
|
+
}
|
|
1178
|
+
processChannelQueue(state) {
|
|
750
1179
|
if (state.processing) {
|
|
751
1180
|
return;
|
|
752
1181
|
}
|
|
@@ -766,17 +1195,24 @@ export class ClaudeCliClient {
|
|
|
766
1195
|
: 'default';
|
|
767
1196
|
const sessionPromptAppend = resolveTurnSessionPromptAppend(turn.preparedTurn);
|
|
768
1197
|
state.visibilityKey = resolveSessionVisibilityKey(turn.preparedTurn.promptContext, sessionPromptAppend, turn.preparedTurn.projectionStrategy);
|
|
1198
|
+
const adoptingPersistentRoute = turn.sessionPersistence === 'persistent'
|
|
1199
|
+
&& state.routingKey !== state.channelId
|
|
1200
|
+
&& !state.persistentFollowUpAdopted;
|
|
1201
|
+
if (adoptingPersistentRoute) {
|
|
1202
|
+
state.persistentFollowUpAdopted = true;
|
|
1203
|
+
state.sessionPersistence = 'persistent';
|
|
1204
|
+
}
|
|
769
1205
|
await this.ensureStarted();
|
|
770
|
-
await this.recycleSessionIfScopeChanged(
|
|
771
|
-
const session = await this.getOrCreateSession(
|
|
1206
|
+
await this.recycleSessionIfScopeChanged(state.routingKey, state);
|
|
1207
|
+
const session = await this.getOrCreateSession(state, turn.preparedTurn, sessionPromptAppend);
|
|
772
1208
|
let reply;
|
|
773
1209
|
try {
|
|
774
1210
|
const promptInput = await this.buildPromptInput(turn.preparedTurn);
|
|
775
|
-
reply = await this.runTurn(session, promptInput, turn.options);
|
|
1211
|
+
reply = await this.runTurn(session, promptInput, turn.options, state.foregroundIsolationFence);
|
|
776
1212
|
}
|
|
777
1213
|
catch (error) {
|
|
778
1214
|
if (isSessionTainted(error)) {
|
|
779
|
-
this.invalidateSession(
|
|
1215
|
+
this.invalidateSession(state.routingKey, state, session);
|
|
780
1216
|
this.rejectQueuedTurnsAfterSessionTaint(state, error);
|
|
781
1217
|
}
|
|
782
1218
|
throw error;
|
|
@@ -793,26 +1229,17 @@ export class ClaudeCliClient {
|
|
|
793
1229
|
}
|
|
794
1230
|
finally {
|
|
795
1231
|
state.processing = false;
|
|
796
|
-
if (state.sessionPersistence === 'ephemeral' && !this.fatalError && !this.disposing && state.queue.length === 0) {
|
|
797
|
-
const session = state.session;
|
|
798
|
-
state.session = undefined;
|
|
799
|
-
state.sessionCwd = undefined;
|
|
800
|
-
state.sessionVisibilityKey = undefined;
|
|
801
|
-
if (session) {
|
|
802
|
-
this.closeSession(session);
|
|
803
|
-
}
|
|
804
|
-
this.channels.delete(channelId);
|
|
805
|
-
}
|
|
806
1232
|
if (state.queue.length > 0 && !this.fatalError) {
|
|
807
|
-
this.processChannelQueue(
|
|
1233
|
+
this.processChannelQueue(state);
|
|
808
1234
|
}
|
|
809
1235
|
else {
|
|
1236
|
+
this.reconcileIdleChannelState(state);
|
|
810
1237
|
this.idleBackendShutdown.reconcile();
|
|
811
1238
|
}
|
|
812
1239
|
}
|
|
813
1240
|
})();
|
|
814
1241
|
}
|
|
815
|
-
async getOrCreateSession(
|
|
1242
|
+
async getOrCreateSession(state, turn, systemPromptAppend) {
|
|
816
1243
|
if (state.session) {
|
|
817
1244
|
return state.session;
|
|
818
1245
|
}
|
|
@@ -824,12 +1251,12 @@ export class ClaudeCliClient {
|
|
|
824
1251
|
}
|
|
825
1252
|
const cwd = state.cwd ?? this.runtime.cwd;
|
|
826
1253
|
const persistedSessionId = state.sessionPersistence === 'persistent'
|
|
827
|
-
? await this.readPersistedSessionId(channelId, cwd, state.visibilityKey)
|
|
1254
|
+
? await this.readPersistedSessionId(state.channelId, cwd, state.visibilityKey)
|
|
828
1255
|
: undefined;
|
|
829
1256
|
const additionalDirectories = resolveSessionAdditionalDirectories(turn.promptContext);
|
|
830
1257
|
const sessionPromise = persistedSessionId
|
|
831
|
-
? this.restoreOrCreateSession(channelId, persistedSessionId, cwd, systemPromptAppend, additionalDirectories)
|
|
832
|
-
: this.startFreshSession(channelId, cwd, systemPromptAppend, additionalDirectories);
|
|
1258
|
+
? this.restoreOrCreateSession(state.channelId, persistedSessionId, cwd, systemPromptAppend, additionalDirectories)
|
|
1259
|
+
: this.startFreshSession(state.channelId, cwd, systemPromptAppend, additionalDirectories);
|
|
833
1260
|
this.pendingSessionStarts.add(sessionPromise);
|
|
834
1261
|
state.sessionPromise = sessionPromise;
|
|
835
1262
|
let sessionAdopted = false;
|
|
@@ -854,10 +1281,16 @@ export class ClaudeCliClient {
|
|
|
854
1281
|
state.session = session;
|
|
855
1282
|
state.sessionCwd = cwd;
|
|
856
1283
|
state.sessionVisibilityKey = state.visibilityKey;
|
|
1284
|
+
if (state.sessionPersistence === 'persistent') {
|
|
1285
|
+
await this.persistSessionBestEffort(state.channelId, session.sessionId, state.visibilityKey, cwd);
|
|
1286
|
+
}
|
|
857
1287
|
return session;
|
|
858
1288
|
}
|
|
859
1289
|
catch (error) {
|
|
860
1290
|
state.session = undefined;
|
|
1291
|
+
state.backgroundTasksActive = false;
|
|
1292
|
+
state.backgroundContinuationPending = false;
|
|
1293
|
+
state.foregroundIsolationFence = undefined;
|
|
861
1294
|
state.sessionCwd = undefined;
|
|
862
1295
|
state.sessionVisibilityKey = undefined;
|
|
863
1296
|
throw error;
|
|
@@ -874,10 +1307,6 @@ export class ClaudeCliClient {
|
|
|
874
1307
|
}
|
|
875
1308
|
const session = await this.connection.agent.buildSession(buildSessionRequest(cwd, systemPromptAppend, additionalDirectories)).start();
|
|
876
1309
|
this.logger?.debug('started fresh Claude ACP session', { channelId, cwd });
|
|
877
|
-
const state = this.channels.get(channelId);
|
|
878
|
-
if (state?.sessionPersistence === 'persistent') {
|
|
879
|
-
await this.persistSessionBestEffort(channelId, session.sessionId, state.visibilityKey, cwd);
|
|
880
|
-
}
|
|
881
1310
|
return session;
|
|
882
1311
|
}
|
|
883
1312
|
async restoreOrCreateSession(channelId, sessionId, cwd, systemPromptAppend, additionalDirectories) {
|
|
@@ -951,6 +1380,11 @@ export class ClaudeCliClient {
|
|
|
951
1380
|
&& state.sessionVisibilityKey === state.visibilityKey) {
|
|
952
1381
|
return;
|
|
953
1382
|
}
|
|
1383
|
+
if (state.backgroundTasksActive
|
|
1384
|
+
|| state.backgroundContinuationPending
|
|
1385
|
+
|| state.sessionHasPendingAutonomousTurn) {
|
|
1386
|
+
throw new Error('Claude session scope cannot change while provider-owned background delivery is running');
|
|
1387
|
+
}
|
|
954
1388
|
this.logger?.debug('recycling Claude ACP session after session scope changed', {
|
|
955
1389
|
channelId,
|
|
956
1390
|
previousCwd: state.sessionCwd,
|
|
@@ -959,57 +1393,142 @@ export class ClaudeCliClient {
|
|
|
959
1393
|
nextVisibilityKey: state.visibilityKey,
|
|
960
1394
|
});
|
|
961
1395
|
state.session = undefined;
|
|
1396
|
+
state.backgroundTasksActive = false;
|
|
1397
|
+
state.backgroundContinuationPending = false;
|
|
1398
|
+
state.sessionHasPendingAutonomousTurn = false;
|
|
1399
|
+
state.foregroundIsolationFence = undefined;
|
|
962
1400
|
state.sessionCwd = undefined;
|
|
963
1401
|
state.sessionVisibilityKey = undefined;
|
|
964
1402
|
if (!this.disposing && !this.backendClosed && state.sessionPersistence === 'persistent') {
|
|
965
|
-
await this.clearPersistedSessionBestEffort(channelId);
|
|
1403
|
+
await this.clearPersistedSessionBestEffort(state.channelId);
|
|
966
1404
|
}
|
|
967
1405
|
this.closeSession(session);
|
|
968
1406
|
}
|
|
969
|
-
async runTurn(session, prompt, options) {
|
|
1407
|
+
async runTurn(session, prompt, options, foregroundIsolationFence) {
|
|
1408
|
+
let updatePump = this.sessionUpdatePumps.get(session);
|
|
1409
|
+
if (!updatePump) {
|
|
1410
|
+
updatePump = new ClaudeSessionUpdatePump(session, (error) => {
|
|
1411
|
+
const binding = [...this.channels.entries()].find(([, candidate]) => candidate.session === session);
|
|
1412
|
+
if (binding) {
|
|
1413
|
+
const [, state] = binding;
|
|
1414
|
+
this.logger?.debug('discarding failed Claude ACP session update pump', {
|
|
1415
|
+
channelId: state.channelId,
|
|
1416
|
+
error: error.message,
|
|
1417
|
+
});
|
|
1418
|
+
this.invalidateSession(state.routingKey, state, session);
|
|
1419
|
+
}
|
|
1420
|
+
}, (active) => {
|
|
1421
|
+
const binding = [...this.channels.entries()].find(([, candidate]) => candidate.session === session);
|
|
1422
|
+
if (!binding) {
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
const [, state] = binding;
|
|
1426
|
+
state.backgroundTasksActive = active;
|
|
1427
|
+
state.backgroundContinuationPending = false;
|
|
1428
|
+
if (active) {
|
|
1429
|
+
this.promoteWorkerRoute(state, session);
|
|
1430
|
+
this.idleBackendShutdown.cancel();
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
this.reconcileIdleChannelState(state);
|
|
1434
|
+
this.idleBackendShutdown.reconcile();
|
|
1435
|
+
}, {
|
|
1436
|
+
onReply: (reply) => {
|
|
1437
|
+
const binding = [...this.channels.entries()].find(([, candidate]) => candidate.session === session);
|
|
1438
|
+
if (!binding) {
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
const [, state] = binding;
|
|
1442
|
+
if (state.session !== session) {
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
this.autonomousReplyHandler?.({
|
|
1446
|
+
channelId: state.channelId,
|
|
1447
|
+
eventId: reply.eventId,
|
|
1448
|
+
text: reply.text,
|
|
1449
|
+
});
|
|
1450
|
+
},
|
|
1451
|
+
onPendingChanged: (active) => {
|
|
1452
|
+
const binding = [...this.channels.entries()].find(([, candidate]) => candidate.session === session);
|
|
1453
|
+
if (!binding) {
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
const [, state] = binding;
|
|
1457
|
+
if (state.session !== session) {
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
state.sessionHasPendingAutonomousTurn = active;
|
|
1461
|
+
if (active) {
|
|
1462
|
+
this.promoteWorkerRoute(state, session);
|
|
1463
|
+
this.idleBackendShutdown.cancel();
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
this.reconcileIdleChannelState(state);
|
|
1467
|
+
this.idleBackendShutdown.reconcile();
|
|
1468
|
+
},
|
|
1469
|
+
}, (foregroundTurn) => {
|
|
1470
|
+
const binding = [...this.channels.entries()].find(([, candidate]) => candidate.session === session);
|
|
1471
|
+
if (!binding) {
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
const [, state] = binding;
|
|
1475
|
+
state.backgroundContinuationPending = true;
|
|
1476
|
+
state.foregroundIsolationFence = Math.max(state.foregroundIsolationFence ?? 0, foregroundTurn);
|
|
1477
|
+
this.promoteWorkerRoute(state, session);
|
|
1478
|
+
this.idleBackendShutdown.cancel();
|
|
1479
|
+
}, this.targetedBackgroundRunCancellationAvailable);
|
|
1480
|
+
this.sessionUpdatePumps.set(session, updatePump);
|
|
1481
|
+
}
|
|
1482
|
+
const turnUpdates = updatePump.beginTurn(options?.onProgress, foregroundIsolationFence);
|
|
970
1483
|
const promptPromise = this.raceWithFatal(session.prompt(prompt));
|
|
971
1484
|
const promptFailure = new Promise((_, reject) => {
|
|
972
1485
|
void promptPromise.catch((error) => reject(markSessionTainted(error)));
|
|
973
1486
|
});
|
|
974
1487
|
const collector = new AcpProgressCollector(options?.onProgress, readClaudeActivityMetadata);
|
|
975
1488
|
const compactionObserver = new ClaudeCompactionObserver(options?.onProgress);
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
update = await this.raceWithFatal(Promise.race([session.nextUpdate(), promptFailure]));
|
|
980
|
-
}
|
|
981
|
-
catch (error) {
|
|
982
|
-
if (this.fatalError && error === this.fatalError) {
|
|
983
|
-
throw error;
|
|
984
|
-
}
|
|
985
|
-
throw markSessionTainted(error);
|
|
986
|
-
}
|
|
987
|
-
if (update.kind === 'stop') {
|
|
988
|
-
let response;
|
|
1489
|
+
try {
|
|
1490
|
+
for (;;) {
|
|
1491
|
+
let update;
|
|
989
1492
|
try {
|
|
990
|
-
|
|
1493
|
+
update = await this.raceWithFatal(Promise.race([turnUpdates.next(), promptFailure]));
|
|
991
1494
|
}
|
|
992
1495
|
catch (error) {
|
|
1496
|
+
if (this.fatalError && error === this.fatalError) {
|
|
1497
|
+
throw error;
|
|
1498
|
+
}
|
|
993
1499
|
throw markSessionTainted(error);
|
|
994
1500
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
}
|
|
1501
|
+
if (update.kind === 'stop') {
|
|
1502
|
+
let response;
|
|
1503
|
+
try {
|
|
1504
|
+
response = await promptPromise;
|
|
1505
|
+
}
|
|
1506
|
+
catch (error) {
|
|
1507
|
+
throw markSessionTainted(error);
|
|
1508
|
+
}
|
|
1509
|
+
const output = collector.getFinalText();
|
|
1510
|
+
if (response.stopReason === 'cancelled') {
|
|
1511
|
+
throw new ProviderTurnCancelledError('Claude ACP turn cancelled', {
|
|
1512
|
+
publicReplyText: hasVisibleText(output) ? output : CLAUDE_CANCELLED_REPLY_TEXT,
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
if (response.stopReason !== 'end_turn' && !hasVisibleText(output)) {
|
|
1516
|
+
throw new Error(`Claude ACP turn stopped with stopReason "${response.stopReason}"`);
|
|
1517
|
+
}
|
|
1518
|
+
return output;
|
|
1000
1519
|
}
|
|
1001
|
-
if (
|
|
1002
|
-
|
|
1520
|
+
if (update.update.sessionUpdate === 'agent_message_chunk' &&
|
|
1521
|
+
update.update.content.type === 'text') {
|
|
1522
|
+
compactionObserver.consumeAnswerChunk(update.update.content.text);
|
|
1003
1523
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
}
|
|
1009
|
-
if (update.update.sessionUpdate === 'usage_update') {
|
|
1010
|
-
compactionObserver.consumeUsageUpdate(update.update.used, update.update.size);
|
|
1524
|
+
if (update.update.sessionUpdate === 'usage_update') {
|
|
1525
|
+
compactionObserver.consumeUsageUpdate(update.update.used, update.update.size);
|
|
1526
|
+
}
|
|
1527
|
+
collector.consume(update);
|
|
1011
1528
|
}
|
|
1012
|
-
|
|
1529
|
+
}
|
|
1530
|
+
finally {
|
|
1531
|
+
turnUpdates.close();
|
|
1013
1532
|
}
|
|
1014
1533
|
}
|
|
1015
1534
|
async raceWithFatal(promise) {
|
|
@@ -1035,15 +1554,21 @@ export class ClaudeCliClient {
|
|
|
1035
1554
|
this.closeSession(state.session);
|
|
1036
1555
|
}
|
|
1037
1556
|
state.session = undefined;
|
|
1557
|
+
state.backgroundTasksActive = false;
|
|
1558
|
+
state.backgroundContinuationPending = false;
|
|
1559
|
+
state.sessionHasPendingAutonomousTurn = false;
|
|
1560
|
+
state.foregroundIsolationFence = undefined;
|
|
1038
1561
|
state.sessionCwd = undefined;
|
|
1039
1562
|
state.sessionVisibilityKey = undefined;
|
|
1040
1563
|
state.sessionPromise = undefined;
|
|
1041
1564
|
state.activeTurn = undefined;
|
|
1565
|
+
state.persistentFollowUpAdopted = false;
|
|
1042
1566
|
if (!state.processing) {
|
|
1043
1567
|
this.channels.delete(channelId);
|
|
1044
1568
|
}
|
|
1045
1569
|
}
|
|
1046
1570
|
this.channels.clear();
|
|
1571
|
+
this.channelWorkerRoutes.clear();
|
|
1047
1572
|
this.shutdownPromise = this.shutdownBackend(error);
|
|
1048
1573
|
}
|
|
1049
1574
|
handlePermissionRequest(params) {
|
|
@@ -1062,13 +1587,103 @@ export class ClaudeCliClient {
|
|
|
1062
1587
|
invalidateSession(channelId, state, session) {
|
|
1063
1588
|
if (state.session === session) {
|
|
1064
1589
|
state.session = undefined;
|
|
1590
|
+
state.backgroundTasksActive = false;
|
|
1591
|
+
state.backgroundContinuationPending = false;
|
|
1592
|
+
state.sessionHasPendingAutonomousTurn = false;
|
|
1593
|
+
state.foregroundIsolationFence = undefined;
|
|
1065
1594
|
state.sessionCwd = undefined;
|
|
1066
1595
|
state.sessionVisibilityKey = undefined;
|
|
1596
|
+
state.persistentFollowUpAdopted = false;
|
|
1067
1597
|
}
|
|
1068
1598
|
this.logger?.debug('discarding tainted Claude ACP session', { channelId });
|
|
1069
1599
|
this.closeSession(session);
|
|
1070
1600
|
if (!this.disposing && !this.backendClosed && state.sessionPersistence === 'persistent') {
|
|
1071
|
-
void this.clearPersistedSessionBestEffort(channelId);
|
|
1601
|
+
void this.clearPersistedSessionBestEffort(state.channelId);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
promoteWorkerRoute(state, session) {
|
|
1605
|
+
this.channelWorkerRoutes.set(state.channelId, {
|
|
1606
|
+
phase: 'promoted',
|
|
1607
|
+
state,
|
|
1608
|
+
});
|
|
1609
|
+
state.sessionPersistence = 'persistent';
|
|
1610
|
+
void (async () => {
|
|
1611
|
+
if (!await this.ensureSessionStoreLoaded()) {
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
const route = this.channelWorkerRoutes.get(state.channelId);
|
|
1615
|
+
if (route?.state !== state
|
|
1616
|
+
|| route.phase !== 'promoted'
|
|
1617
|
+
|| state.session !== session
|
|
1618
|
+
|| (!state.backgroundTasksActive && !state.sessionHasPendingAutonomousTurn && !state.persistentFollowUpAdopted)) {
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
await this.persistSessionBestEffort(state.channelId, session.sessionId, state.visibilityKey, state.sessionCwd ?? state.cwd ?? this.runtime.cwd);
|
|
1622
|
+
})().catch((error) => {
|
|
1623
|
+
this.logger.error('failed to initialize Claude session persistence for background work', {
|
|
1624
|
+
agentId: this.loadedSessionStoreAgentId,
|
|
1625
|
+
channelId: state.channelId,
|
|
1626
|
+
sessionId: session.sessionId,
|
|
1627
|
+
error: summarizeError(error),
|
|
1628
|
+
});
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
releaseWorkerRoute(state, clearPersistent = false) {
|
|
1632
|
+
const route = this.channelWorkerRoutes.get(state.channelId);
|
|
1633
|
+
if (route?.state !== state) {
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
this.channelWorkerRoutes.delete(state.channelId);
|
|
1637
|
+
if (clearPersistent && state.session) {
|
|
1638
|
+
void this.clearPersistedSessionBestEffort(state.channelId);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
reconcileIdleChannelState(state) {
|
|
1642
|
+
if (this.channels.get(state.routingKey) !== state
|
|
1643
|
+
|| this.disposing
|
|
1644
|
+
|| this.fatalError
|
|
1645
|
+
|| this.backendClosed
|
|
1646
|
+
|| state.processing
|
|
1647
|
+
|| state.activeTurn
|
|
1648
|
+
|| state.queue.length > 0
|
|
1649
|
+
|| state.sessionPromise) {
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
const route = this.channelWorkerRoutes.get(state.channelId);
|
|
1653
|
+
if (route?.state === state
|
|
1654
|
+
&& route.phase === 'promoted'
|
|
1655
|
+
&& !state.backgroundTasksActive
|
|
1656
|
+
&& !state.backgroundContinuationPending
|
|
1657
|
+
&& !state.sessionHasPendingAutonomousTurn) {
|
|
1658
|
+
if (state.routingKey === state.channelId) {
|
|
1659
|
+
this.releaseWorkerRoute(state);
|
|
1660
|
+
}
|
|
1661
|
+
else if (!state.persistentFollowUpAdopted) {
|
|
1662
|
+
state.sessionPersistence = 'ephemeral';
|
|
1663
|
+
this.releaseWorkerRoute(state, true);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
else if (route?.state === state
|
|
1667
|
+
&& route.phase === 'candidate'
|
|
1668
|
+
&& (state.routingKey === state.channelId
|
|
1669
|
+
|| state.sessionPersistence === 'ephemeral')) {
|
|
1670
|
+
this.releaseWorkerRoute(state);
|
|
1671
|
+
}
|
|
1672
|
+
if (state.sessionPersistence !== 'ephemeral'
|
|
1673
|
+
|| state.backgroundTasksActive
|
|
1674
|
+
|| state.backgroundContinuationPending
|
|
1675
|
+
|| state.sessionHasPendingAutonomousTurn) {
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
const session = state.session;
|
|
1679
|
+
state.session = undefined;
|
|
1680
|
+
state.foregroundIsolationFence = undefined;
|
|
1681
|
+
state.sessionCwd = undefined;
|
|
1682
|
+
state.sessionVisibilityKey = undefined;
|
|
1683
|
+
this.releaseWorkerRoute(state);
|
|
1684
|
+
this.channels.delete(state.routingKey);
|
|
1685
|
+
if (session) {
|
|
1686
|
+
this.closeSession(session);
|
|
1072
1687
|
}
|
|
1073
1688
|
}
|
|
1074
1689
|
closeSession(session) {
|
|
@@ -1076,6 +1691,8 @@ export class ClaudeCliClient {
|
|
|
1076
1691
|
return;
|
|
1077
1692
|
}
|
|
1078
1693
|
this.closingSessions.add(session);
|
|
1694
|
+
this.sessionUpdatePumps.get(session)?.close();
|
|
1695
|
+
this.sessionUpdatePumps.delete(session);
|
|
1079
1696
|
const agent = this.connection?.agent;
|
|
1080
1697
|
const closeMethod = this.runtime.methods.agent.session?.close;
|
|
1081
1698
|
const closePromise = typeof agent?.closeSession === 'function'
|
|
@@ -1261,10 +1878,16 @@ export class ClaudeCliClient {
|
|
|
1261
1878
|
sessions.push(state.session);
|
|
1262
1879
|
}
|
|
1263
1880
|
state.session = undefined;
|
|
1881
|
+
state.backgroundTasksActive = false;
|
|
1882
|
+
state.backgroundContinuationPending = false;
|
|
1883
|
+
state.sessionHasPendingAutonomousTurn = false;
|
|
1884
|
+
state.foregroundIsolationFence = undefined;
|
|
1264
1885
|
state.sessionCwd = undefined;
|
|
1265
1886
|
state.sessionVisibilityKey = undefined;
|
|
1887
|
+
state.persistentFollowUpAdopted = false;
|
|
1266
1888
|
this.channels.delete(channelId);
|
|
1267
1889
|
}
|
|
1890
|
+
this.channelWorkerRoutes.clear();
|
|
1268
1891
|
this.logger?.debug('stopping idle Claude ACP backend', {
|
|
1269
1892
|
channels: sessions.length,
|
|
1270
1893
|
idleShutdownMs: this.runtime.idleBackendShutdownMs,
|
|
@@ -1276,6 +1899,7 @@ export class ClaudeCliClient {
|
|
|
1276
1899
|
this.startPromise = undefined;
|
|
1277
1900
|
this.childExitPromise = undefined;
|
|
1278
1901
|
this.sessionCapabilities = DEFAULT_SESSION_CAPABILITIES;
|
|
1902
|
+
this.targetedBackgroundRunCancellationAvailable = false;
|
|
1279
1903
|
this.idleShutdownPromise = undefined;
|
|
1280
1904
|
});
|
|
1281
1905
|
this.idleShutdownPromise = idleShutdownPromise;
|
|
@@ -1292,7 +1916,13 @@ export class ClaudeCliClient {
|
|
|
1292
1916
|
return false;
|
|
1293
1917
|
}
|
|
1294
1918
|
for (const state of this.channels.values()) {
|
|
1295
|
-
if (state.activeTurn ||
|
|
1919
|
+
if (state.activeTurn ||
|
|
1920
|
+
state.backgroundTasksActive ||
|
|
1921
|
+
state.backgroundContinuationPending ||
|
|
1922
|
+
state.sessionHasPendingAutonomousTurn ||
|
|
1923
|
+
state.queue.length > 0 ||
|
|
1924
|
+
state.sessionPromise ||
|
|
1925
|
+
state.processing) {
|
|
1296
1926
|
return false;
|
|
1297
1927
|
}
|
|
1298
1928
|
}
|