@borgee/agents-host 0.2.29 → 0.2.32
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 +82 -27
- package/dist/agents-host.d.ts +1 -1
- package/dist/agents-host.js +45 -18
- package/dist/cli-args.d.ts +3 -1
- package/dist/cli-args.js +18 -2
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +12 -1
- package/dist/compatibility-gates.js +4 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.js +51 -13
- package/dist/context/prompt.js +1 -1
- package/dist/gateway/localhost-gateway.js +34 -2
- package/dist/index.js +4 -1
- package/dist/local-config.js +20 -1
- package/dist/managed-daemon.js +20 -6
- package/dist/policy/authorization-audit.d.ts +1 -0
- package/dist/policy/gateway-authorization.d.ts +1 -1
- package/dist/policy/gateway-authorization.js +27 -4
- package/dist/providers/claude/cli-client.d.ts +55 -16
- package/dist/providers/claude/cli-client.js +811 -345
- package/dist/providers/create-provider.js +8 -13
- package/dist/state-paths.d.ts +5 -0
- package/dist/state-paths.js +12 -0
- package/dist/types.d.ts +1 -0
- package/dist/update/package-installation.d.ts +52 -0
- package/dist/update/package-installation.js +90 -0
- package/dist/update/package-manager.d.ts +33 -0
- package/dist/update/package-manager.js +137 -0
- package/dist/update/semantic-version.d.ts +7 -0
- package/dist/update/semantic-version.js +76 -0
- package/dist/update/update-command.d.ts +9 -0
- package/dist/update/update-command.js +55 -0
- package/dist/update/update-notice.d.ts +39 -0
- package/dist/update/update-notice.js +161 -0
- package/package.json +5 -2
- package/skills/borgee-agent/SKILL.md +5 -3
- package/skills/borgee-agent/borgee-agent.mjs +56 -22
- package/skills/borgee-agent/borgee-agent.py +56 -27
|
@@ -1,49 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Readable, Writable } from 'node:stream';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
3
4
|
import spawn from 'cross-spawn';
|
|
5
|
+
import { assertClaudeCommandCompatibility, DEFAULT_CLAUDE_COMMAND, isLegacyClaudeCompatibilityAlias, } from '../../config.js';
|
|
6
|
+
import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from '@agentclientprotocol/sdk';
|
|
4
7
|
import { HostLogger, summarizeChildStderr, summarizeError } from '../../debug.js';
|
|
8
|
+
import { resolveCopilotPermissionResponse } from '../../policy/copilot-permission.js';
|
|
9
|
+
const SESSION_TAINTED_ERRORS = new WeakSet();
|
|
10
|
+
const DEFAULT_SHUTDOWN_GRACE_PERIOD_MS = 250;
|
|
11
|
+
const DEFAULT_SHUTDOWN_FORCE_KILL_WAIT_MS = 250;
|
|
12
|
+
const DEFAULT_SESSION_CAPABILITIES = {
|
|
13
|
+
loadSession: false,
|
|
14
|
+
resumeSession: false,
|
|
15
|
+
};
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
5
17
|
const DEFAULT_RUNTIME = {
|
|
6
18
|
spawn,
|
|
19
|
+
client,
|
|
20
|
+
ndJsonStream,
|
|
21
|
+
methods,
|
|
22
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
7
23
|
cwd: process.cwd(),
|
|
24
|
+
shutdownGracePeriodMs: DEFAULT_SHUTDOWN_GRACE_PERIOD_MS,
|
|
25
|
+
shutdownForceKillWaitMs: DEFAULT_SHUTDOWN_FORCE_KILL_WAIT_MS,
|
|
8
26
|
};
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
exitCode;
|
|
12
|
-
signal;
|
|
13
|
-
stderrBytes;
|
|
14
|
-
stderrLineCount;
|
|
15
|
-
staleResume;
|
|
16
|
-
constructor(exitCode, signal, stderrBytes, stderrLineCount, staleResume = false) {
|
|
17
|
-
super(formatClaudeCliFailureMessage(exitCode, signal, stderrBytes, stderrLineCount));
|
|
18
|
-
this.name = 'ClaudeCliProcessError';
|
|
19
|
-
if (exitCode !== null) {
|
|
20
|
-
this.exitCode = exitCode;
|
|
21
|
-
}
|
|
22
|
-
if (signal !== null) {
|
|
23
|
-
this.signal = signal;
|
|
24
|
-
}
|
|
25
|
-
this.stderrBytes = stderrBytes;
|
|
26
|
-
this.stderrLineCount = stderrLineCount;
|
|
27
|
-
this.staleResume = staleResume;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
function formatClaudeCliFailureMessage(exitCode, signal, stderrBytes, stderrLineCount) {
|
|
31
|
-
const processOutcome = signal ? `signal ${signal}` : `code ${String(exitCode ?? 'unknown')}`;
|
|
32
|
-
const lineLabel = stderrLineCount === 1 ? 'line' : 'lines';
|
|
33
|
-
return `Claude CLI failed with ${processOutcome} (stderr: ${stderrBytes} bytes across ${stderrLineCount} ${lineLabel})`;
|
|
34
|
-
}
|
|
35
|
-
function matchesStaleResumeText(text) {
|
|
36
|
-
const normalized = text.toLowerCase();
|
|
37
|
-
return normalized.includes('session not found')
|
|
38
|
-
|| normalized.includes('cannot resume')
|
|
39
|
-
|| normalized.includes('no conversation found with session id');
|
|
40
|
-
}
|
|
41
|
-
function isStaleResumeFailure(error) {
|
|
42
|
-
if (error instanceof ClaudeCliProcessError) {
|
|
43
|
-
return error.staleResume;
|
|
44
|
-
}
|
|
45
|
-
const message = normalizeError(error).message.toLowerCase();
|
|
46
|
-
return matchesStaleResumeText(message);
|
|
27
|
+
function hasVisibleText(value) {
|
|
28
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
47
29
|
}
|
|
48
30
|
function normalizeError(error) {
|
|
49
31
|
return error instanceof Error ? error : new Error(String(error));
|
|
@@ -58,6 +40,9 @@ function parsePersistedSessionRecord(rawValue) {
|
|
|
58
40
|
cwd: typeof parsed.cwd === 'string' && parsed.cwd.trim().length > 0
|
|
59
41
|
? parsed.cwd
|
|
60
42
|
: undefined,
|
|
43
|
+
visibilityKey: typeof parsed.visibilityKey === 'string' && parsed.visibilityKey.trim().length > 0
|
|
44
|
+
? parsed.visibilityKey
|
|
45
|
+
: undefined,
|
|
61
46
|
};
|
|
62
47
|
}
|
|
63
48
|
}
|
|
@@ -67,15 +52,117 @@ function serializePersistedSessionRecord(record) {
|
|
|
67
52
|
return JSON.stringify({
|
|
68
53
|
sessionId: record.sessionId,
|
|
69
54
|
...(record.cwd ? { cwd: record.cwd } : {}),
|
|
55
|
+
...(record.visibilityKey ? { visibilityKey: record.visibilityKey } : {}),
|
|
70
56
|
});
|
|
71
57
|
}
|
|
58
|
+
function formatClaudeAcpFailureMessage(exitCode, signal, stderrBytes, stderrLineCount) {
|
|
59
|
+
const processOutcome = signal ? `signal ${signal}` : `code ${String(exitCode ?? 'unknown')}`;
|
|
60
|
+
const lineLabel = stderrLineCount === 1 ? 'line' : 'lines';
|
|
61
|
+
return `Claude ACP process failed with ${processOutcome} (stderr: ${stderrBytes} bytes across ${stderrLineCount} ${lineLabel})`;
|
|
62
|
+
}
|
|
63
|
+
class ClaudeCliProcessError extends Error {
|
|
64
|
+
exitCode;
|
|
65
|
+
signal;
|
|
66
|
+
stderrBytes;
|
|
67
|
+
stderrLineCount;
|
|
68
|
+
constructor(exitCode, signal, stderrBytes, stderrLineCount) {
|
|
69
|
+
super(formatClaudeAcpFailureMessage(exitCode, signal, stderrBytes, stderrLineCount));
|
|
70
|
+
this.name = 'ClaudeCliProcessError';
|
|
71
|
+
if (exitCode !== null) {
|
|
72
|
+
this.exitCode = exitCode;
|
|
73
|
+
}
|
|
74
|
+
if (signal !== null) {
|
|
75
|
+
this.signal = signal;
|
|
76
|
+
}
|
|
77
|
+
this.stderrBytes = stderrBytes;
|
|
78
|
+
this.stderrLineCount = stderrLineCount;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function formatToolProgress(title, status) {
|
|
82
|
+
const normalizedTitle = hasVisibleText(title) ? title.trim() : null;
|
|
83
|
+
switch (status) {
|
|
84
|
+
case 'completed':
|
|
85
|
+
return normalizedTitle ? `Completed ${normalizedTitle}` : 'Completed tool call';
|
|
86
|
+
case 'failed':
|
|
87
|
+
return normalizedTitle ? `Failed ${normalizedTitle}` : 'Tool call failed';
|
|
88
|
+
case 'pending':
|
|
89
|
+
case 'in_progress':
|
|
90
|
+
case undefined:
|
|
91
|
+
case null:
|
|
92
|
+
return normalizedTitle ? `Running ${normalizedTitle}…` : 'Running tool…';
|
|
93
|
+
default:
|
|
94
|
+
return normalizedTitle ? `${status} ${normalizedTitle}` : status;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function formatPlanProgress(entries) {
|
|
98
|
+
const current = entries.find((entry) => entry.status === 'in_progress') ??
|
|
99
|
+
entries.find((entry) => entry.status === 'pending') ??
|
|
100
|
+
entries[0];
|
|
101
|
+
return hasVisibleText(current?.content) ? `Plan: ${current.content.trim()}` : null;
|
|
102
|
+
}
|
|
103
|
+
class ClaudeProgressCollector {
|
|
104
|
+
onProgress;
|
|
105
|
+
publicText = '';
|
|
106
|
+
lastPublished = null;
|
|
107
|
+
toolTitles = new Map();
|
|
108
|
+
constructor(onProgress) {
|
|
109
|
+
this.onProgress = onProgress;
|
|
110
|
+
}
|
|
111
|
+
consume(update) {
|
|
112
|
+
switch (update.update.sessionUpdate) {
|
|
113
|
+
case 'agent_message_chunk':
|
|
114
|
+
if (update.update.content.type !== 'text') {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.publicText += update.update.content.text;
|
|
118
|
+
this.publish(this.publicText);
|
|
119
|
+
return;
|
|
120
|
+
case 'tool_call':
|
|
121
|
+
this.toolTitles.set(update.update.toolCallId, update.update.title);
|
|
122
|
+
this.publishFallback(formatToolProgress(update.update.title, update.update.status));
|
|
123
|
+
return;
|
|
124
|
+
case 'tool_call_update': {
|
|
125
|
+
const nextTitle = update.update.title ?? this.toolTitles.get(update.update.toolCallId);
|
|
126
|
+
if (hasVisibleText(nextTitle)) {
|
|
127
|
+
this.toolTitles.set(update.update.toolCallId, nextTitle);
|
|
128
|
+
}
|
|
129
|
+
this.publishFallback(formatToolProgress(nextTitle, update.update.status));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
case 'plan':
|
|
133
|
+
this.publishFallback(formatPlanProgress(update.update.entries.map((entry) => ({
|
|
134
|
+
content: entry.content,
|
|
135
|
+
status: entry.status,
|
|
136
|
+
}))));
|
|
137
|
+
return;
|
|
138
|
+
default:
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
getFinalText() {
|
|
143
|
+
return this.publicText.trim();
|
|
144
|
+
}
|
|
145
|
+
publishFallback(text) {
|
|
146
|
+
if (hasVisibleText(this.publicText)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.publish(text);
|
|
150
|
+
}
|
|
151
|
+
publish(text) {
|
|
152
|
+
if (!this.onProgress || !hasVisibleText(text) || text === this.lastPublished) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
this.lastPublished = text;
|
|
156
|
+
this.onProgress({ text });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
72
159
|
function createDeferredTurn(channelId, prompt, sessionPersistence, promptContext, options) {
|
|
73
160
|
let settled = false;
|
|
74
161
|
let resolvePromise;
|
|
75
162
|
let rejectPromise;
|
|
76
|
-
const promise = new Promise((
|
|
77
|
-
resolvePromise =
|
|
78
|
-
rejectPromise =
|
|
163
|
+
const promise = new Promise((resolvePromiseParam, rejectPromiseParam) => {
|
|
164
|
+
resolvePromise = resolvePromiseParam;
|
|
165
|
+
rejectPromise = rejectPromiseParam;
|
|
79
166
|
});
|
|
80
167
|
return {
|
|
81
168
|
channelId,
|
|
@@ -98,145 +185,171 @@ function createDeferredTurn(channelId, prompt, sessionPersistence, promptContext
|
|
|
98
185
|
},
|
|
99
186
|
};
|
|
100
187
|
}
|
|
188
|
+
function resolveSessionRouting(turn) {
|
|
189
|
+
return {
|
|
190
|
+
channelId: turn.channelId,
|
|
191
|
+
key: turn.providerSessionRouting?.key ?? turn.channelId,
|
|
192
|
+
persistence: turn.providerSessionRouting?.persistence ?? 'persistent',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
101
195
|
function asObject(value) {
|
|
102
196
|
return typeof value === 'object' && value !== null ? value : null;
|
|
103
197
|
}
|
|
104
|
-
function
|
|
105
|
-
|
|
198
|
+
function readSessionCapabilities(response) {
|
|
199
|
+
const agentCapabilities = asObject(asObject(response)?.agentCapabilities);
|
|
200
|
+
const sessionCapabilities = asObject(agentCapabilities?.sessionCapabilities ?? agentCapabilities?.session);
|
|
201
|
+
return {
|
|
202
|
+
loadSession: agentCapabilities?.loadSession === true,
|
|
203
|
+
resumeSession: asObject(sessionCapabilities?.resume) !== null,
|
|
204
|
+
};
|
|
106
205
|
}
|
|
107
|
-
function
|
|
108
|
-
|
|
206
|
+
function isStaleRestoreFailure(error) {
|
|
207
|
+
const normalized = normalizeError(error);
|
|
208
|
+
const code = asObject(error)?.code;
|
|
209
|
+
const dataCode = asObject(asObject(error)?.data)?.code;
|
|
210
|
+
if (code === 'session_not_found' || dataCode === 'session_not_found') {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
const message = normalized.message.toLowerCase();
|
|
214
|
+
return message.includes('session not found')
|
|
215
|
+
|| message.includes('unknown session')
|
|
216
|
+
|| message.includes('cannot resume')
|
|
217
|
+
|| message.includes('no conversation found with session id')
|
|
218
|
+
|| message.includes('not found');
|
|
219
|
+
}
|
|
220
|
+
function markSessionTainted(error) {
|
|
221
|
+
const normalized = normalizeError(error);
|
|
222
|
+
SESSION_TAINTED_ERRORS.add(normalized);
|
|
223
|
+
return normalized;
|
|
224
|
+
}
|
|
225
|
+
function isSessionTainted(error) {
|
|
226
|
+
return error instanceof Error && SESSION_TAINTED_ERRORS.has(error);
|
|
227
|
+
}
|
|
228
|
+
function resolveBundledClaudeAdapterPath() {
|
|
229
|
+
return require.resolve('@agentclientprotocol/claude-agent-acp/dist/index.js');
|
|
109
230
|
}
|
|
110
231
|
function isRelativePathLike(value) {
|
|
111
232
|
return value.startsWith('./') || value.startsWith('../');
|
|
112
233
|
}
|
|
234
|
+
function useBundledClaudeAdapter(command, args) {
|
|
235
|
+
return command === DEFAULT_CLAUDE_COMMAND;
|
|
236
|
+
}
|
|
237
|
+
function useLegacyClaudeCompatibilityAlias(command, args) {
|
|
238
|
+
return isLegacyClaudeCompatibilityAlias(command, args);
|
|
239
|
+
}
|
|
113
240
|
function resolveClaudeLaunch(command, args, baseCwd) {
|
|
114
|
-
|
|
115
|
-
|
|
241
|
+
if (useBundledClaudeAdapter(command, args)) {
|
|
242
|
+
return {
|
|
243
|
+
command: process.execPath,
|
|
244
|
+
args: [resolveBundledClaudeAdapterPath(), ...args],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (useLegacyClaudeCompatibilityAlias(command, args)) {
|
|
248
|
+
return {
|
|
249
|
+
command: process.execPath,
|
|
250
|
+
args: [resolveBundledClaudeAdapterPath()],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
116
253
|
return {
|
|
117
|
-
command:
|
|
118
|
-
args:
|
|
254
|
+
command: isRelativePathLike(command) ? resolve(baseCwd, command) : command,
|
|
255
|
+
args: args.map((arg) => (isRelativePathLike(arg) ? resolve(baseCwd, arg) : arg)),
|
|
119
256
|
};
|
|
120
257
|
}
|
|
121
|
-
function
|
|
122
|
-
if (!
|
|
123
|
-
return
|
|
258
|
+
function buildClaudeSessionSystemPrompt(promptContext) {
|
|
259
|
+
if (!promptContext) {
|
|
260
|
+
return undefined;
|
|
124
261
|
}
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
continue;
|
|
130
|
-
if (block.type === 'text' && hasVisibleText(asString(block.text))) {
|
|
131
|
-
segments.push(asString(block.text));
|
|
132
|
-
}
|
|
262
|
+
const lines = [];
|
|
263
|
+
if (promptContext.channelContextPayloadPath) {
|
|
264
|
+
lines.push('Channel-scoped local context is available for this session.');
|
|
265
|
+
lines.push(`Channel context payload: ${promptContext.channelContextPayloadPath}`);
|
|
133
266
|
}
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const delta = asObject(event.delta);
|
|
139
|
-
if (!delta)
|
|
140
|
-
return null;
|
|
141
|
-
const deltaType = asString(delta.type);
|
|
142
|
-
if (deltaType !== undefined && deltaType !== 'text_delta') {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
return asString(delta.text) ?? null;
|
|
267
|
+
if (promptContext.skillRuntime) {
|
|
268
|
+
lines.push(`Skill guide: ${promptContext.skillRuntime.skillMarkdownPath}`);
|
|
269
|
+
lines.push(`Node bootstrap CLI: node ${promptContext.skillRuntime.nodeCliPath} --context ${promptContext.channelContextPayloadPath} --print-bootstrap`);
|
|
270
|
+
lines.push(`Python bootstrap CLI: python3 ${promptContext.skillRuntime.pythonCliPath} --context ${promptContext.channelContextPayloadPath} --print-bootstrap`);
|
|
146
271
|
}
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
if (
|
|
150
|
-
|
|
272
|
+
if (promptContext.localhostGateway && promptContext.gatewayAuthPath) {
|
|
273
|
+
lines.push(`Gateway auth sidecar for this session: ${promptContext.gatewayAuthPath}`);
|
|
274
|
+
if (promptContext.localhostGateway.baseUrl) {
|
|
275
|
+
lines.push(`Loopback gateway base URL: ${promptContext.localhostGateway.baseUrl}`);
|
|
151
276
|
}
|
|
152
|
-
return asString(block.text) ?? null;
|
|
153
277
|
}
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (event.type === 'tool_use') {
|
|
158
|
-
const toolName = asString(event.name) ?? asString(event.tool_name);
|
|
159
|
-
return hasVisibleText(toolName) ? `Running ${toolName}…` : 'Running tool…';
|
|
278
|
+
if (promptContext.taskWorkspace) {
|
|
279
|
+
lines.push(`Task-scoped writable workspace root: ${promptContext.taskWorkspace.rootPath}.`);
|
|
280
|
+
lines.push('This writable workspace is local to agents-host for the current task thread and does not imply that the target repository is already checked out there.');
|
|
160
281
|
}
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
if (!block || block.type !== 'tool_use') {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
const toolName = asString(block.name);
|
|
167
|
-
return hasVisibleText(toolName) ? `Running ${toolName}…` : 'Running tool…';
|
|
282
|
+
if (promptContext.taskAssignmentContext?.currentTaskId) {
|
|
283
|
+
lines.push(`Current assigned task id: ${promptContext.taskAssignmentContext.currentTaskId}.`);
|
|
168
284
|
}
|
|
169
|
-
return
|
|
285
|
+
return lines.length > 0 ? lines.join('\n') : undefined;
|
|
170
286
|
}
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return result;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (event.type === 'message' || event.type === 'assistant') {
|
|
179
|
-
const message = asObject(event.message) ?? event;
|
|
180
|
-
const joined = readTextBlocks(message.content).join('');
|
|
181
|
-
if (joined.length > 0) {
|
|
182
|
-
return joined;
|
|
183
|
-
}
|
|
287
|
+
function buildClaudeSessionMeta(promptContext) {
|
|
288
|
+
const systemPromptAppend = buildClaudeSessionSystemPrompt(promptContext);
|
|
289
|
+
if (!systemPromptAppend) {
|
|
290
|
+
return undefined;
|
|
184
291
|
}
|
|
185
|
-
return
|
|
292
|
+
return {
|
|
293
|
+
systemPrompt: {
|
|
294
|
+
append: systemPromptAppend,
|
|
295
|
+
},
|
|
296
|
+
};
|
|
186
297
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
lastPublished = null;
|
|
192
|
-
constructor(onProgress) {
|
|
193
|
-
this.onProgress = onProgress;
|
|
298
|
+
function resolveSessionAdditionalDirectories(promptContext) {
|
|
299
|
+
const directories = new Set();
|
|
300
|
+
if (promptContext?.channelContextPayloadPath) {
|
|
301
|
+
directories.add(dirname(promptContext.channelContextPayloadPath));
|
|
194
302
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const textDelta = extractTextDelta(payload);
|
|
198
|
-
if (textDelta !== null) {
|
|
199
|
-
this.publicText += textDelta;
|
|
200
|
-
this.publish(this.publicText);
|
|
201
|
-
}
|
|
202
|
-
if (!hasVisibleText(this.publicText)) {
|
|
203
|
-
const toolSummary = extractToolSummary(payload);
|
|
204
|
-
if (toolSummary) {
|
|
205
|
-
this.publish(toolSummary);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
const finalText = extractFinalText(event) ?? extractFinalText(payload);
|
|
209
|
-
if (finalText !== null) {
|
|
210
|
-
this.finalText = finalText;
|
|
211
|
-
}
|
|
303
|
+
if (promptContext?.gatewayAuthPath) {
|
|
304
|
+
directories.add(dirname(promptContext.gatewayAuthPath));
|
|
212
305
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return hasVisibleText(text) ? text : '';
|
|
306
|
+
if (promptContext?.skillRuntime?.skillDirectoryPath) {
|
|
307
|
+
directories.add(promptContext.skillRuntime.skillDirectoryPath);
|
|
216
308
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
309
|
+
return directories.size > 0 ? [...directories].sort((left, right) => left.localeCompare(right)) : undefined;
|
|
310
|
+
}
|
|
311
|
+
function resolveSessionVisibilityKey(promptContext) {
|
|
312
|
+
const additionalDirectories = resolveSessionAdditionalDirectories(promptContext);
|
|
313
|
+
if (!additionalDirectories) {
|
|
314
|
+
return undefined;
|
|
223
315
|
}
|
|
316
|
+
return JSON.stringify({ additionalDirectories });
|
|
224
317
|
}
|
|
225
|
-
function
|
|
318
|
+
function buildSessionRequest(cwd, promptContext, additionalDirectories) {
|
|
319
|
+
const meta = buildClaudeSessionMeta(promptContext);
|
|
226
320
|
return {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
321
|
+
cwd,
|
|
322
|
+
mcpServers: [],
|
|
323
|
+
...(additionalDirectories && additionalDirectories.length > 0
|
|
324
|
+
? { additionalDirectories }
|
|
325
|
+
: {}),
|
|
326
|
+
...(meta ? { _meta: meta } : {}),
|
|
230
327
|
};
|
|
231
328
|
}
|
|
329
|
+
function shouldReusePersistedSession(record, cwd, runtimeCwd, visibilityKey) {
|
|
330
|
+
if (record.cwd) {
|
|
331
|
+
if (record.cwd !== cwd) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else if (cwd !== runtimeCwd) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
if (visibilityKey !== undefined) {
|
|
339
|
+
return record.visibilityKey === visibilityKey;
|
|
340
|
+
}
|
|
341
|
+
if (record.visibilityKey !== undefined) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
232
346
|
/**
|
|
233
|
-
*
|
|
234
|
-
* native provider-session continuity.
|
|
347
|
+
* Persistent ACP-backed client for the Claude ACP adapter.
|
|
235
348
|
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
349
|
+
* The public caller surface intentionally stays stable: callers still create a
|
|
350
|
+
* `ClaudeCliClient`, then call `generateReply()` and `dispose()`. Internally,
|
|
351
|
+
* the shipped runtime now runs one Claude ACP adapter process and reuses one
|
|
352
|
+
* ACP session per routed Borgee channel.
|
|
240
353
|
*/
|
|
241
354
|
export class ClaudeCliClient {
|
|
242
355
|
command;
|
|
@@ -247,21 +360,42 @@ export class ClaudeCliClient {
|
|
|
247
360
|
runtime;
|
|
248
361
|
channels = new Map();
|
|
249
362
|
persistedSessions = new Map();
|
|
363
|
+
closingSessions = new WeakSet();
|
|
364
|
+
pendingSessionStarts = new Set();
|
|
365
|
+
pendingSessionCloses = new Set();
|
|
366
|
+
pendingSessionStoreOperations = new Set();
|
|
367
|
+
fatalPromise;
|
|
368
|
+
rejectFatalPromise;
|
|
369
|
+
child;
|
|
370
|
+
connection;
|
|
371
|
+
startPromise;
|
|
372
|
+
shutdownPromise;
|
|
373
|
+
childExitPromise;
|
|
374
|
+
resolveChildExit;
|
|
375
|
+
fatalError = null;
|
|
376
|
+
disposing = false;
|
|
377
|
+
backendClosed = false;
|
|
250
378
|
loadedSessionStoreAgentId = null;
|
|
251
|
-
stopped = false;
|
|
252
379
|
sessionStoreLoadPromise = null;
|
|
253
380
|
sessionStoreWriteQueue = Promise.resolve();
|
|
254
|
-
|
|
381
|
+
sessionCapabilities = DEFAULT_SESSION_CAPABILITIES;
|
|
382
|
+
childStderr = '';
|
|
383
|
+
constructor(command, args = [], runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined, logger = new HostLogger()) {
|
|
255
384
|
this.command = command;
|
|
256
385
|
this.args = args;
|
|
257
386
|
this.sessionStore = sessionStore;
|
|
258
387
|
this.resolveSessionStoreAgentId = resolveSessionStoreAgentId;
|
|
259
388
|
this.logger = logger;
|
|
260
389
|
this.runtime = { ...DEFAULT_RUNTIME, ...runtimeOverrides };
|
|
390
|
+
assertClaudeCommandCompatibility(command, args, 'Claude provider runtime');
|
|
391
|
+
this.fatalPromise = new Promise((_, reject) => {
|
|
392
|
+
this.rejectFatalPromise = reject;
|
|
393
|
+
});
|
|
394
|
+
void this.fatalPromise.catch(() => { });
|
|
261
395
|
}
|
|
262
396
|
async generateReply(channelIdOrTurn, promptOrOptions, maybeOptions) {
|
|
263
|
-
if (this.
|
|
264
|
-
throw
|
|
397
|
+
if (this.fatalError) {
|
|
398
|
+
throw this.fatalError;
|
|
265
399
|
}
|
|
266
400
|
const preparedTurn = typeof channelIdOrTurn === 'string'
|
|
267
401
|
? {
|
|
@@ -273,42 +407,145 @@ export class ClaudeCliClient {
|
|
|
273
407
|
? maybeOptions
|
|
274
408
|
: promptOrOptions;
|
|
275
409
|
const sessionRouting = resolveSessionRouting(preparedTurn);
|
|
276
|
-
const state = this.getOrCreateChannelState(sessionRouting.key, sessionRouting.persistence);
|
|
277
|
-
const
|
|
278
|
-
state.queue.push(
|
|
410
|
+
const state = this.getOrCreateChannelState(sessionRouting.key, preparedTurn.channelId, sessionRouting.persistence);
|
|
411
|
+
const turn = createDeferredTurn(preparedTurn.channelId, preparedTurn.prompt, sessionRouting.persistence, preparedTurn.promptContext, options);
|
|
412
|
+
state.queue.push(turn);
|
|
279
413
|
this.processChannelQueue(sessionRouting.key, state);
|
|
280
|
-
return
|
|
414
|
+
return turn.promise;
|
|
281
415
|
}
|
|
282
416
|
async dispose() {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
this.
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
417
|
+
const error = new Error('Claude ACP backend stopped');
|
|
418
|
+
this.disposing = true;
|
|
419
|
+
this.logger?.debug('stopping Claude ACP backend');
|
|
420
|
+
const closed = this.connection?.closed ?? Promise.resolve();
|
|
421
|
+
this.failAll(error);
|
|
422
|
+
let thrown;
|
|
423
|
+
try {
|
|
424
|
+
await Promise.all([closed, this.shutdownPromise ?? Promise.resolve()]);
|
|
425
|
+
}
|
|
426
|
+
catch (disposeError) {
|
|
427
|
+
thrown = disposeError;
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
await Promise.allSettled(this.pendingSessionStoreOperations);
|
|
431
|
+
await this.sessionStoreWriteQueue;
|
|
432
|
+
await this.sessionStore?.close?.();
|
|
433
|
+
}
|
|
434
|
+
catch (closeError) {
|
|
435
|
+
thrown ??= closeError;
|
|
436
|
+
}
|
|
437
|
+
if (thrown) {
|
|
438
|
+
throw thrown;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async ensureStarted() {
|
|
442
|
+
if (this.fatalError) {
|
|
443
|
+
throw this.fatalError;
|
|
444
|
+
}
|
|
445
|
+
if (!this.startPromise) {
|
|
446
|
+
this.startPromise = this.startBackend();
|
|
447
|
+
}
|
|
448
|
+
await this.startPromise;
|
|
449
|
+
if (this.fatalError) {
|
|
450
|
+
throw this.fatalError;
|
|
451
|
+
}
|
|
452
|
+
if (!this.connection) {
|
|
453
|
+
throw new Error('Claude ACP connection is not available');
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
async startBackend() {
|
|
457
|
+
const launch = resolveClaudeLaunch(this.command, this.args, this.runtime.cwd);
|
|
458
|
+
this.childStderr = '';
|
|
459
|
+
this.logger?.debug('starting Claude ACP process', {
|
|
460
|
+
command: launch.command,
|
|
461
|
+
args: launch.args,
|
|
462
|
+
cwd: this.runtime.cwd,
|
|
463
|
+
});
|
|
464
|
+
const child = this.runtime.spawn(launch.command, launch.args, {
|
|
465
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
466
|
+
cwd: this.runtime.cwd,
|
|
467
|
+
});
|
|
468
|
+
this.child = child;
|
|
469
|
+
child.stderr.setEncoding('utf8');
|
|
470
|
+
child.stderr.on('data', (chunk) => {
|
|
471
|
+
this.childStderr += chunk;
|
|
472
|
+
this.logger?.childStderr('claude stderr', summarizeChildStderr(chunk));
|
|
473
|
+
});
|
|
474
|
+
child.stderr.resume();
|
|
475
|
+
this.logger?.debug('spawned Claude ACP process', { pid: child.pid });
|
|
476
|
+
child.once('error', (error) => {
|
|
477
|
+
this.logger?.debugError('Claude ACP process error', error);
|
|
478
|
+
this.failAll(new Error(`Claude ACP process error: ${normalizeError(error).message}`));
|
|
479
|
+
});
|
|
480
|
+
child.once('exit', (code, signal) => {
|
|
481
|
+
this.resolveChildExit?.();
|
|
482
|
+
if (this.disposing) {
|
|
483
|
+
this.logger?.debug('Claude ACP process exited during shutdown', { code, signal });
|
|
484
|
+
return;
|
|
291
485
|
}
|
|
292
|
-
|
|
293
|
-
|
|
486
|
+
const stderrSummary = summarizeChildStderr(this.childStderr);
|
|
487
|
+
const failure = new ClaudeCliProcessError(code, signal, stderrSummary.bytes, stderrSummary.lineCount);
|
|
488
|
+
this.logger?.debugError('Claude ACP process exited unexpectedly', {
|
|
489
|
+
code,
|
|
490
|
+
signal,
|
|
491
|
+
stderrBytes: stderrSummary.bytes,
|
|
492
|
+
stderrLineCount: stderrSummary.lineCount,
|
|
493
|
+
});
|
|
494
|
+
this.failAll(failure);
|
|
495
|
+
});
|
|
496
|
+
this.childExitPromise = new Promise((resolveChildExit) => {
|
|
497
|
+
this.resolveChildExit = resolveChildExit;
|
|
498
|
+
});
|
|
499
|
+
const output = Writable.toWeb(child.stdin);
|
|
500
|
+
const input = Readable.toWeb(child.stdout);
|
|
501
|
+
const stream = this.runtime.ndJsonStream(output, input);
|
|
502
|
+
const app = this.runtime
|
|
503
|
+
.client({ name: 'borgee-agents-host' })
|
|
504
|
+
.onRequest(this.runtime.methods.client.session.requestPermission, ({ params }) => (this.handlePermissionRequest(params)));
|
|
505
|
+
const connection = app.connect(stream);
|
|
506
|
+
this.connection = connection;
|
|
507
|
+
void connection.closed.then(() => {
|
|
508
|
+
if (!this.disposing) {
|
|
509
|
+
this.logger?.debugError('Claude ACP connection closed unexpectedly');
|
|
510
|
+
this.failAll(new Error('Claude ACP connection closed unexpectedly'));
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
try {
|
|
514
|
+
const initializeResponse = await connection.agent.request(this.runtime.methods.agent.initialize, {
|
|
515
|
+
protocolVersion: this.runtime.protocolVersion,
|
|
516
|
+
clientCapabilities: {},
|
|
517
|
+
clientInfo: {
|
|
518
|
+
name: '@borgee/agents-host',
|
|
519
|
+
version: '0.2.29',
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
this.sessionCapabilities = readSessionCapabilities(initializeResponse);
|
|
523
|
+
this.logger?.debug('initialized Claude ACP connection', {
|
|
524
|
+
loadSession: this.sessionCapabilities.loadSession,
|
|
525
|
+
resumeSession: this.sessionCapabilities.resumeSession,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
const normalized = new Error(`Claude ACP initialize failed: ${normalizeError(error).message}`);
|
|
530
|
+
this.logger?.debugError('Claude ACP initialize failed', normalized);
|
|
531
|
+
this.failAll(normalized);
|
|
532
|
+
throw normalized;
|
|
294
533
|
}
|
|
295
|
-
await this.sessionStoreWriteQueue;
|
|
296
|
-
await this.sessionStore?.close?.();
|
|
297
534
|
}
|
|
298
|
-
getOrCreateChannelState(channelId, sessionPersistence) {
|
|
535
|
+
getOrCreateChannelState(channelId, resolvedChannelId, sessionPersistence) {
|
|
299
536
|
let state = this.channels.get(channelId);
|
|
300
537
|
if (!state) {
|
|
301
538
|
state = {
|
|
539
|
+
channelId: resolvedChannelId,
|
|
302
540
|
sessionPersistence,
|
|
303
541
|
processing: false,
|
|
304
542
|
queue: [],
|
|
305
|
-
sessionHydrated: false,
|
|
306
|
-
sessionEstablished: false,
|
|
307
543
|
};
|
|
308
544
|
this.channels.set(channelId, state);
|
|
309
545
|
}
|
|
310
|
-
else if (state.
|
|
311
|
-
|
|
546
|
+
else if (state.channelId !== resolvedChannelId
|
|
547
|
+
|| state.sessionPersistence !== sessionPersistence) {
|
|
548
|
+
throw new Error(`Claude session routing changed for key "${channelId}"`);
|
|
312
549
|
}
|
|
313
550
|
return state;
|
|
314
551
|
}
|
|
@@ -319,28 +556,30 @@ export class ClaudeCliClient {
|
|
|
319
556
|
state.processing = true;
|
|
320
557
|
void (async () => {
|
|
321
558
|
try {
|
|
322
|
-
while (!this.
|
|
323
|
-
const queuedTurn = state.queue[0];
|
|
324
|
-
if (!queuedTurn) {
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
try {
|
|
328
|
-
if (state.sessionPersistence === 'persistent' && await this.ensureSessionStoreLoaded()) {
|
|
329
|
-
this.hydrateChannelState(channelId, state);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
catch (error) {
|
|
333
|
-
state.queue.shift()?.reject(error);
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
559
|
+
while (!this.fatalError) {
|
|
336
560
|
const turn = state.queue.shift();
|
|
337
561
|
if (!turn) {
|
|
338
562
|
return;
|
|
339
563
|
}
|
|
340
564
|
state.activeTurn = turn;
|
|
341
565
|
try {
|
|
342
|
-
|
|
343
|
-
turn.
|
|
566
|
+
state.cwd = this.resolveSessionCwd(turn.promptContext);
|
|
567
|
+
state.visibilityKey = resolveSessionVisibilityKey(turn.promptContext);
|
|
568
|
+
await this.ensureStarted();
|
|
569
|
+
await this.recycleSessionIfScopeChanged(channelId, state);
|
|
570
|
+
const session = await this.getOrCreateSession(channelId, state, turn.promptContext);
|
|
571
|
+
let reply;
|
|
572
|
+
try {
|
|
573
|
+
reply = await this.runTurn(session, turn.prompt, turn.options);
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
if (isSessionTainted(error)) {
|
|
577
|
+
this.invalidateSession(channelId, state, session);
|
|
578
|
+
this.rejectQueuedTurnsAfterSessionTaint(state, error);
|
|
579
|
+
}
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
582
|
+
turn.resolve(reply);
|
|
344
583
|
}
|
|
345
584
|
catch (error) {
|
|
346
585
|
turn.reject(error);
|
|
@@ -352,54 +591,309 @@ export class ClaudeCliClient {
|
|
|
352
591
|
}
|
|
353
592
|
finally {
|
|
354
593
|
state.processing = false;
|
|
355
|
-
if (state.sessionPersistence === 'ephemeral' && !this.
|
|
356
|
-
|
|
357
|
-
state.
|
|
358
|
-
state.sessionId = undefined;
|
|
594
|
+
if (state.sessionPersistence === 'ephemeral' && !this.fatalError && !this.disposing && state.queue.length === 0) {
|
|
595
|
+
const session = state.session;
|
|
596
|
+
state.session = undefined;
|
|
359
597
|
state.sessionCwd = undefined;
|
|
598
|
+
state.sessionVisibilityKey = undefined;
|
|
599
|
+
if (session) {
|
|
600
|
+
this.closeSession(session);
|
|
601
|
+
}
|
|
360
602
|
this.channels.delete(channelId);
|
|
361
603
|
}
|
|
362
|
-
if (state.queue.length > 0 && !this.
|
|
604
|
+
if (state.queue.length > 0 && !this.fatalError) {
|
|
363
605
|
this.processChannelQueue(channelId, state);
|
|
364
606
|
}
|
|
365
607
|
}
|
|
366
608
|
})();
|
|
367
609
|
}
|
|
368
|
-
async
|
|
369
|
-
|
|
370
|
-
|
|
610
|
+
async getOrCreateSession(channelId, state, promptContext) {
|
|
611
|
+
if (state.session) {
|
|
612
|
+
return state.session;
|
|
613
|
+
}
|
|
614
|
+
if (state.sessionPromise) {
|
|
615
|
+
return state.sessionPromise;
|
|
616
|
+
}
|
|
617
|
+
if (!this.connection) {
|
|
618
|
+
throw new Error('Claude ACP connection is not available');
|
|
619
|
+
}
|
|
620
|
+
const cwd = state.cwd ?? this.runtime.cwd;
|
|
621
|
+
const persistedSessionId = state.sessionPersistence === 'persistent'
|
|
622
|
+
? await this.readPersistedSessionId(channelId, cwd, state.visibilityKey)
|
|
623
|
+
: undefined;
|
|
624
|
+
const additionalDirectories = resolveSessionAdditionalDirectories(promptContext);
|
|
625
|
+
const sessionPromise = persistedSessionId
|
|
626
|
+
? this.restoreOrCreateSession(channelId, persistedSessionId, cwd, promptContext, additionalDirectories)
|
|
627
|
+
: this.startFreshSession(channelId, cwd, promptContext, additionalDirectories);
|
|
628
|
+
this.pendingSessionStarts.add(sessionPromise);
|
|
629
|
+
state.sessionPromise = sessionPromise;
|
|
630
|
+
let sessionAdopted = false;
|
|
631
|
+
void sessionPromise
|
|
632
|
+
.then((session) => {
|
|
633
|
+
if (!sessionAdopted &&
|
|
634
|
+
(this.fatalError !== null || this.disposing || state.sessionPromise !== sessionPromise)) {
|
|
635
|
+
this.closeSession(session);
|
|
636
|
+
}
|
|
637
|
+
})
|
|
638
|
+
.finally(() => {
|
|
639
|
+
this.pendingSessionStarts.delete(sessionPromise);
|
|
640
|
+
})
|
|
641
|
+
.catch(() => { });
|
|
642
|
+
try {
|
|
643
|
+
const session = await this.raceWithFatal(sessionPromise);
|
|
644
|
+
if (this.fatalError) {
|
|
645
|
+
this.closeSession(session);
|
|
646
|
+
throw this.fatalError;
|
|
647
|
+
}
|
|
648
|
+
sessionAdopted = true;
|
|
649
|
+
state.session = session;
|
|
650
|
+
state.sessionCwd = cwd;
|
|
651
|
+
state.sessionVisibilityKey = state.visibilityKey;
|
|
652
|
+
return session;
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
state.session = undefined;
|
|
656
|
+
state.sessionCwd = undefined;
|
|
657
|
+
state.sessionVisibilityKey = undefined;
|
|
658
|
+
throw error;
|
|
659
|
+
}
|
|
660
|
+
finally {
|
|
661
|
+
if (state.sessionPromise === sessionPromise) {
|
|
662
|
+
state.sessionPromise = undefined;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async startFreshSession(channelId, cwd, promptContext, additionalDirectories) {
|
|
667
|
+
if (!this.connection) {
|
|
668
|
+
throw new Error('Claude ACP connection is not available');
|
|
669
|
+
}
|
|
670
|
+
const session = await this.connection.agent.buildSession(buildSessionRequest(cwd, promptContext, additionalDirectories)).start();
|
|
671
|
+
this.logger?.debug('started fresh Claude ACP session', { channelId, cwd });
|
|
672
|
+
const state = this.channels.get(channelId);
|
|
673
|
+
if (state?.sessionPersistence === 'persistent') {
|
|
674
|
+
await this.persistSessionBestEffort(channelId, session.sessionId, state.visibilityKey, cwd);
|
|
675
|
+
}
|
|
676
|
+
return session;
|
|
677
|
+
}
|
|
678
|
+
async restoreOrCreateSession(channelId, sessionId, cwd, promptContext, additionalDirectories) {
|
|
679
|
+
if (!this.sessionCapabilities.resumeSession) {
|
|
680
|
+
this.logger?.debug('Claude ACP restore unsupported; starting fresh session', { channelId });
|
|
681
|
+
return this.startFreshSession(channelId, cwd, promptContext, additionalDirectories);
|
|
682
|
+
}
|
|
683
|
+
try {
|
|
684
|
+
const session = await this.restoreSession(sessionId, cwd, promptContext, additionalDirectories);
|
|
685
|
+
this.logger?.debug('restored Claude ACP session', {
|
|
686
|
+
channelId,
|
|
687
|
+
restoreMethod: this.sessionCapabilities.resumeSession ? 'session/resume' : 'session/load',
|
|
688
|
+
});
|
|
689
|
+
return session;
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
if (!isStaleRestoreFailure(error)) {
|
|
693
|
+
throw normalizeError(error);
|
|
694
|
+
}
|
|
695
|
+
this.logger?.debug('discarded stale Claude ACP session and started fresh', { channelId });
|
|
696
|
+
await this.clearPersistedSessionBestEffort(channelId);
|
|
697
|
+
return this.startFreshSession(channelId, cwd, promptContext, additionalDirectories);
|
|
698
|
+
}
|
|
371
699
|
}
|
|
372
|
-
async
|
|
373
|
-
if (this.
|
|
374
|
-
throw new Error('Claude
|
|
700
|
+
async restoreSession(sessionId, cwd, promptContext, additionalDirectories) {
|
|
701
|
+
if (!this.connection) {
|
|
702
|
+
throw new Error('Claude ACP connection is not available');
|
|
703
|
+
}
|
|
704
|
+
const agent = this.connection.agent;
|
|
705
|
+
if (typeof agent.attachSession !== 'function') {
|
|
706
|
+
throw new Error('Claude ACP SDK does not expose session attachment helpers');
|
|
375
707
|
}
|
|
376
|
-
const
|
|
377
|
-
const sessionArgs = state.sessionEstablished ? ['--resume', sessionId] : ['--session-id', sessionId];
|
|
708
|
+
const session = agent.attachSession({ sessionId });
|
|
378
709
|
try {
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (
|
|
384
|
-
await this.
|
|
710
|
+
const request = {
|
|
711
|
+
sessionId,
|
|
712
|
+
...buildSessionRequest(cwd, promptContext, additionalDirectories),
|
|
713
|
+
};
|
|
714
|
+
if (this.sessionCapabilities.resumeSession) {
|
|
715
|
+
await this.connection.agent.request(this.runtime.methods.agent.session.resume, request);
|
|
716
|
+
return session;
|
|
717
|
+
}
|
|
718
|
+
if (this.sessionCapabilities.loadSession) {
|
|
719
|
+
await this.connection.agent.request(this.runtime.methods.agent.session.load, request);
|
|
720
|
+
this.clearBufferedSessionReplay(session);
|
|
721
|
+
return session;
|
|
385
722
|
}
|
|
386
|
-
|
|
723
|
+
throw new Error('Claude ACP agent does not advertise session restore capabilities');
|
|
387
724
|
}
|
|
388
725
|
catch (error) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
726
|
+
session.dispose();
|
|
727
|
+
throw normalizeError(error);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
clearBufferedSessionReplay(session) {
|
|
731
|
+
const updates = session.updates;
|
|
732
|
+
if (updates && Array.isArray(updates.values)) {
|
|
733
|
+
updates.values = [];
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
resolveSessionCwd(promptContext) {
|
|
737
|
+
return promptContext?.taskWorkspace?.rootPath ?? this.runtime.cwd;
|
|
738
|
+
}
|
|
739
|
+
async recycleSessionIfScopeChanged(channelId, state) {
|
|
740
|
+
const session = state.session;
|
|
741
|
+
if (!session) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const nextCwd = state.cwd ?? this.runtime.cwd;
|
|
745
|
+
if (state.sessionCwd === nextCwd
|
|
746
|
+
&& state.sessionVisibilityKey === state.visibilityKey) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
this.logger?.debug('recycling Claude ACP session after session scope changed', {
|
|
750
|
+
channelId,
|
|
751
|
+
previousCwd: state.sessionCwd,
|
|
752
|
+
nextCwd,
|
|
753
|
+
previousVisibilityKey: state.sessionVisibilityKey,
|
|
754
|
+
nextVisibilityKey: state.visibilityKey,
|
|
755
|
+
});
|
|
756
|
+
state.session = undefined;
|
|
757
|
+
state.sessionCwd = undefined;
|
|
758
|
+
state.sessionVisibilityKey = undefined;
|
|
759
|
+
if (!this.disposing && !this.backendClosed && state.sessionPersistence === 'persistent') {
|
|
760
|
+
await this.clearPersistedSessionBestEffort(channelId);
|
|
761
|
+
}
|
|
762
|
+
this.closeSession(session);
|
|
763
|
+
}
|
|
764
|
+
async runTurn(session, prompt, options) {
|
|
765
|
+
const promptPromise = this.raceWithFatal(session.prompt(prompt));
|
|
766
|
+
const promptFailure = new Promise((_, reject) => {
|
|
767
|
+
void promptPromise.catch((error) => reject(markSessionTainted(error)));
|
|
768
|
+
});
|
|
769
|
+
const collector = new ClaudeProgressCollector(options?.onProgress);
|
|
770
|
+
for (;;) {
|
|
771
|
+
let update;
|
|
772
|
+
try {
|
|
773
|
+
update = await this.raceWithFatal(Promise.race([session.nextUpdate(), promptFailure]));
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
if (this.fatalError && error === this.fatalError) {
|
|
777
|
+
throw error;
|
|
392
778
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
779
|
+
throw markSessionTainted(error);
|
|
780
|
+
}
|
|
781
|
+
if (update.kind === 'stop') {
|
|
782
|
+
let response;
|
|
783
|
+
try {
|
|
784
|
+
response = await promptPromise;
|
|
396
785
|
}
|
|
397
|
-
|
|
398
|
-
|
|
786
|
+
catch (error) {
|
|
787
|
+
throw markSessionTainted(error);
|
|
399
788
|
}
|
|
789
|
+
const output = collector.getFinalText();
|
|
790
|
+
if (response.stopReason !== 'end_turn' && !hasVisibleText(output)) {
|
|
791
|
+
throw new Error(`Claude ACP turn stopped with stopReason "${response.stopReason}"`);
|
|
792
|
+
}
|
|
793
|
+
return output;
|
|
794
|
+
}
|
|
795
|
+
collector.consume(update);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
async raceWithFatal(promise) {
|
|
799
|
+
if (this.fatalError) {
|
|
800
|
+
throw this.fatalError;
|
|
801
|
+
}
|
|
802
|
+
return Promise.race([promise, this.fatalPromise]);
|
|
803
|
+
}
|
|
804
|
+
failAll(error) {
|
|
805
|
+
if (this.fatalError) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
this.fatalError = error;
|
|
809
|
+
this.rejectFatalPromise(error);
|
|
810
|
+
for (const [channelId, state] of this.channels.entries()) {
|
|
811
|
+
state.activeTurn?.reject(error);
|
|
812
|
+
for (const turn of state.queue) {
|
|
813
|
+
turn.reject(error);
|
|
814
|
+
}
|
|
815
|
+
state.queue.length = 0;
|
|
816
|
+
if (state.session) {
|
|
817
|
+
this.closeSession(state.session);
|
|
818
|
+
}
|
|
819
|
+
state.session = undefined;
|
|
820
|
+
state.sessionCwd = undefined;
|
|
821
|
+
state.sessionVisibilityKey = undefined;
|
|
822
|
+
state.sessionPromise = undefined;
|
|
823
|
+
state.activeTurn = undefined;
|
|
824
|
+
if (!state.processing) {
|
|
825
|
+
this.channels.delete(channelId);
|
|
400
826
|
}
|
|
401
|
-
throw error;
|
|
402
827
|
}
|
|
828
|
+
this.channels.clear();
|
|
829
|
+
this.shutdownPromise = this.shutdownBackend(error);
|
|
830
|
+
}
|
|
831
|
+
handlePermissionRequest(params) {
|
|
832
|
+
return resolveCopilotPermissionResponse({
|
|
833
|
+
gateEnabled: true,
|
|
834
|
+
policyMode: 'enforce',
|
|
835
|
+
params,
|
|
836
|
+
logger: this.logger,
|
|
837
|
+
agentId: this.resolveSessionStoreAgentId()?.trim() || undefined,
|
|
838
|
+
channelId: this.findChannelIdBySessionId(params.sessionId),
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
invalidateSession(channelId, state, session) {
|
|
842
|
+
if (state.session === session) {
|
|
843
|
+
state.session = undefined;
|
|
844
|
+
state.sessionCwd = undefined;
|
|
845
|
+
state.sessionVisibilityKey = undefined;
|
|
846
|
+
}
|
|
847
|
+
this.logger?.debug('discarding tainted Claude ACP session', { channelId });
|
|
848
|
+
this.closeSession(session);
|
|
849
|
+
if (!this.disposing && !this.backendClosed && state.sessionPersistence === 'persistent') {
|
|
850
|
+
void this.clearPersistedSessionBestEffort(channelId);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
closeSession(session) {
|
|
854
|
+
if (this.closingSessions.has(session)) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
this.closingSessions.add(session);
|
|
858
|
+
const agent = this.connection?.agent;
|
|
859
|
+
const closeMethod = this.runtime.methods.agent.session?.close;
|
|
860
|
+
const closePromise = typeof agent?.closeSession === 'function'
|
|
861
|
+
? agent.closeSession({ sessionId: session.sessionId })
|
|
862
|
+
: closeMethod
|
|
863
|
+
? this.connection?.agent.request(closeMethod, { sessionId: session.sessionId })
|
|
864
|
+
: undefined;
|
|
865
|
+
session.dispose();
|
|
866
|
+
if (!closePromise) {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
let trackedClosePromise;
|
|
870
|
+
trackedClosePromise = Promise.resolve(closePromise)
|
|
871
|
+
.then(() => undefined)
|
|
872
|
+
.catch(() => { })
|
|
873
|
+
.finally(() => {
|
|
874
|
+
this.pendingSessionCloses.delete(trackedClosePromise);
|
|
875
|
+
});
|
|
876
|
+
this.pendingSessionCloses.add(trackedClosePromise);
|
|
877
|
+
}
|
|
878
|
+
rejectQueuedTurnsAfterSessionTaint(state, error) {
|
|
879
|
+
if (state.queue.length === 0) {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const rejection = new Error('Claude ACP session was reset after a failed turn; queued turns were dropped instead of replaying them on a fresh session', {
|
|
883
|
+
cause: normalizeError(error),
|
|
884
|
+
});
|
|
885
|
+
for (const turn of state.queue) {
|
|
886
|
+
turn.reject(rejection);
|
|
887
|
+
}
|
|
888
|
+
state.queue.length = 0;
|
|
889
|
+
}
|
|
890
|
+
findChannelIdBySessionId(sessionId) {
|
|
891
|
+
for (const state of this.channels.values()) {
|
|
892
|
+
if (state.session?.sessionId === sessionId) {
|
|
893
|
+
return state.channelId;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return undefined;
|
|
403
897
|
}
|
|
404
898
|
currentSessionStoreAgentId() {
|
|
405
899
|
const agentId = this.resolveSessionStoreAgentId()?.trim();
|
|
@@ -417,9 +911,6 @@ export class ClaudeCliClient {
|
|
|
417
911
|
this.loadedSessionStoreAgentId = null;
|
|
418
912
|
this.sessionStoreLoadPromise = null;
|
|
419
913
|
this.persistedSessions.clear();
|
|
420
|
-
for (const state of this.channels.values()) {
|
|
421
|
-
state.sessionHydrated = false;
|
|
422
|
-
}
|
|
423
914
|
}
|
|
424
915
|
if (!this.sessionStoreLoadPromise) {
|
|
425
916
|
this.sessionStoreLoadPromise = (async () => {
|
|
@@ -440,38 +931,41 @@ export class ClaudeCliClient {
|
|
|
440
931
|
await this.sessionStoreLoadPromise;
|
|
441
932
|
return true;
|
|
442
933
|
}
|
|
443
|
-
|
|
444
|
-
if (
|
|
445
|
-
return;
|
|
934
|
+
async readPersistedSessionId(channelId, cwd, visibilityKey) {
|
|
935
|
+
if (!await this.ensureSessionStoreLoaded()) {
|
|
936
|
+
return undefined;
|
|
446
937
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
return;
|
|
938
|
+
const record = this.persistedSessions.get(channelId);
|
|
939
|
+
if (!record) {
|
|
940
|
+
return undefined;
|
|
451
941
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
942
|
+
if (!shouldReusePersistedSession(record, cwd, this.runtime.cwd, visibilityKey)) {
|
|
943
|
+
await this.clearPersistedSessionBestEffort(channelId);
|
|
944
|
+
return undefined;
|
|
945
|
+
}
|
|
946
|
+
return record.sessionId;
|
|
455
947
|
}
|
|
456
|
-
async persistSession(channelId, sessionId, cwd) {
|
|
948
|
+
async persistSession(channelId, sessionId, visibilityKey, cwd) {
|
|
457
949
|
if (!this.sessionStore || !this.loadedSessionStoreAgentId) {
|
|
458
950
|
return;
|
|
459
951
|
}
|
|
460
952
|
await this.enqueueSessionStoreWrite(async () => {
|
|
461
953
|
const current = this.persistedSessions.get(channelId);
|
|
462
|
-
if (current?.sessionId === sessionId
|
|
954
|
+
if (current?.sessionId === sessionId
|
|
955
|
+
&& current.cwd === cwd
|
|
956
|
+
&& current.visibilityKey === visibilityKey) {
|
|
463
957
|
return;
|
|
464
958
|
}
|
|
465
|
-
this.persistedSessions.set(channelId, { sessionId, cwd });
|
|
959
|
+
this.persistedSessions.set(channelId, { sessionId, cwd, visibilityKey });
|
|
466
960
|
await this.sessionStore.save(this.loadedSessionStoreAgentId, Object.fromEntries([...this.persistedSessions.entries()].map(([persistedChannelId, record]) => [
|
|
467
961
|
persistedChannelId,
|
|
468
962
|
serializePersistedSessionRecord(record),
|
|
469
963
|
])));
|
|
470
964
|
});
|
|
471
965
|
}
|
|
472
|
-
async persistSessionBestEffort(channelId, sessionId, cwd) {
|
|
966
|
+
async persistSessionBestEffort(channelId, sessionId, visibilityKey, cwd) {
|
|
473
967
|
try {
|
|
474
|
-
await this.persistSession(channelId, sessionId, cwd);
|
|
968
|
+
await this.trackSessionStoreOperation(this.persistSession(channelId, sessionId, visibilityKey, cwd));
|
|
475
969
|
}
|
|
476
970
|
catch (error) {
|
|
477
971
|
this.logger.error('failed to persist Claude session map; keeping reply delivery', {
|
|
@@ -482,10 +976,7 @@ export class ClaudeCliClient {
|
|
|
482
976
|
});
|
|
483
977
|
}
|
|
484
978
|
}
|
|
485
|
-
async
|
|
486
|
-
state.sessionEstablished = false;
|
|
487
|
-
state.sessionId = undefined;
|
|
488
|
-
state.sessionCwd = undefined;
|
|
979
|
+
async clearPersistedSession(channelId) {
|
|
489
980
|
if (!this.sessionStore || !this.loadedSessionStoreAgentId) {
|
|
490
981
|
return;
|
|
491
982
|
}
|
|
@@ -500,28 +991,9 @@ export class ClaudeCliClient {
|
|
|
500
991
|
])));
|
|
501
992
|
});
|
|
502
993
|
}
|
|
503
|
-
|
|
504
|
-
return turn.promptContext?.taskWorkspace?.rootPath ?? this.runtime.cwd;
|
|
505
|
-
}
|
|
506
|
-
async resetSessionIfCwdChanged(channelId, state, turn) {
|
|
507
|
-
if (!state.sessionEstablished) {
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
const nextCwd = this.resolveTurnCwd(turn);
|
|
511
|
-
if (state.sessionCwd === nextCwd) {
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
if (turn.sessionPersistence === 'persistent') {
|
|
515
|
-
await this.resetPersistedSessionBestEffort(channelId, state);
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
state.sessionEstablished = false;
|
|
519
|
-
state.sessionId = undefined;
|
|
520
|
-
state.sessionCwd = undefined;
|
|
521
|
-
}
|
|
522
|
-
async resetPersistedSessionBestEffort(channelId, state) {
|
|
994
|
+
async clearPersistedSessionBestEffort(channelId) {
|
|
523
995
|
try {
|
|
524
|
-
await this.
|
|
996
|
+
await this.trackSessionStoreOperation(this.clearPersistedSession(channelId));
|
|
525
997
|
}
|
|
526
998
|
catch (error) {
|
|
527
999
|
this.logger.error('failed to clear stale Claude session map; retrying in-memory only', {
|
|
@@ -532,88 +1004,82 @@ export class ClaudeCliClient {
|
|
|
532
1004
|
}
|
|
533
1005
|
}
|
|
534
1006
|
async enqueueSessionStoreWrite(writeOperation) {
|
|
535
|
-
const
|
|
536
|
-
this.sessionStoreWriteQueue =
|
|
537
|
-
await
|
|
538
|
-
}
|
|
539
|
-
async
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const child = this.runtime.spawn(launch.command, launch.args, {
|
|
543
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
544
|
-
cwd: promptContext?.taskWorkspace?.rootPath ?? this.runtime.cwd,
|
|
545
|
-
});
|
|
546
|
-
state.activeChild = child;
|
|
547
|
-
const collector = new ClaudeStreamCollector(options?.onProgress);
|
|
548
|
-
let stderr = '';
|
|
549
|
-
let lineBuffer = '';
|
|
550
|
-
let settled = false;
|
|
551
|
-
const settleReject = (error) => {
|
|
552
|
-
if (settled)
|
|
553
|
-
return;
|
|
554
|
-
settled = true;
|
|
555
|
-
reject(normalizeError(error));
|
|
556
|
-
};
|
|
557
|
-
const settleResolve = (value) => {
|
|
558
|
-
if (settled)
|
|
559
|
-
return;
|
|
560
|
-
settled = true;
|
|
561
|
-
resolve(value);
|
|
562
|
-
};
|
|
563
|
-
child.stdout.setEncoding('utf8');
|
|
564
|
-
child.stderr.setEncoding('utf8');
|
|
565
|
-
child.stdout.on('data', (chunk) => {
|
|
566
|
-
lineBuffer += chunk;
|
|
567
|
-
while (true) {
|
|
568
|
-
const newlineIndex = lineBuffer.indexOf('\n');
|
|
569
|
-
if (newlineIndex === -1) {
|
|
570
|
-
break;
|
|
571
|
-
}
|
|
572
|
-
const line = lineBuffer.slice(0, newlineIndex).trim();
|
|
573
|
-
lineBuffer = lineBuffer.slice(newlineIndex + 1);
|
|
574
|
-
if (!line)
|
|
575
|
-
continue;
|
|
576
|
-
try {
|
|
577
|
-
const event = JSON.parse(line);
|
|
578
|
-
collector.consume(event);
|
|
579
|
-
}
|
|
580
|
-
catch (error) {
|
|
581
|
-
settleReject(new Error(`Claude stream-json parse failed: ${normalizeError(error).message}`));
|
|
582
|
-
child.kill('SIGTERM');
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
});
|
|
587
|
-
child.stderr.on('data', (chunk) => {
|
|
588
|
-
stderr += chunk;
|
|
589
|
-
this.logger?.childStderr('claude stderr', summarizeChildStderr(chunk));
|
|
590
|
-
});
|
|
591
|
-
child.once('error', (error) => {
|
|
592
|
-
settleReject(error);
|
|
593
|
-
});
|
|
594
|
-
child.once('close', (code, signal) => {
|
|
595
|
-
if (state.activeChild === child) {
|
|
596
|
-
state.activeChild = undefined;
|
|
597
|
-
}
|
|
598
|
-
if (lineBuffer.trim().length > 0 && !settled) {
|
|
599
|
-
try {
|
|
600
|
-
collector.consume(JSON.parse(lineBuffer.trim()));
|
|
601
|
-
lineBuffer = '';
|
|
602
|
-
}
|
|
603
|
-
catch (error) {
|
|
604
|
-
settleReject(new Error(`Claude stream-json parse failed: ${normalizeError(error).message}`));
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
if (code !== 0) {
|
|
609
|
-
const stderrSummary = summarizeChildStderr(stderr);
|
|
610
|
-
settleReject(new ClaudeCliProcessError(code, signal, stderrSummary.bytes, stderrSummary.lineCount, matchesStaleResumeText(stderr)));
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
settleResolve(collector.getFinalText());
|
|
614
|
-
});
|
|
615
|
-
child.stdin.write(prompt);
|
|
616
|
-
child.stdin.end();
|
|
1007
|
+
const write = this.sessionStoreWriteQueue.then(writeOperation);
|
|
1008
|
+
this.sessionStoreWriteQueue = write.catch(() => { });
|
|
1009
|
+
await write;
|
|
1010
|
+
}
|
|
1011
|
+
async trackSessionStoreOperation(operation) {
|
|
1012
|
+
const tracked = operation.finally(() => {
|
|
1013
|
+
this.pendingSessionStoreOperations.delete(tracked);
|
|
617
1014
|
});
|
|
1015
|
+
this.pendingSessionStoreOperations.add(tracked);
|
|
1016
|
+
return tracked;
|
|
1017
|
+
}
|
|
1018
|
+
async shutdownBackend(error) {
|
|
1019
|
+
if (this.backendClosed) {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
this.backendClosed = true;
|
|
1023
|
+
const connection = this.connection;
|
|
1024
|
+
const child = this.child;
|
|
1025
|
+
await this.waitForPendingSessionStarts();
|
|
1026
|
+
await this.waitForPendingSessionCloses();
|
|
1027
|
+
connection?.close(error);
|
|
1028
|
+
this.connection = undefined;
|
|
1029
|
+
if (!child) {
|
|
1030
|
+
this.child = undefined;
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const childExitPromise = this.childExitPromise ?? Promise.resolve();
|
|
1034
|
+
this.logger?.debug('sending SIGTERM to Claude ACP process');
|
|
1035
|
+
child.kill('SIGTERM');
|
|
1036
|
+
const exitedAfterTerm = await this.waitForChildExit(childExitPromise, this.runtime.shutdownGracePeriodMs);
|
|
1037
|
+
if (exitedAfterTerm) {
|
|
1038
|
+
this.logger?.debug('Claude ACP process exited after SIGTERM');
|
|
1039
|
+
this.child = undefined;
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
this.logger?.debug('sending SIGKILL to Claude ACP process after SIGTERM grace timeout');
|
|
1043
|
+
child.kill('SIGKILL');
|
|
1044
|
+
await this.waitForChildExit(childExitPromise, this.runtime.shutdownForceKillWaitMs);
|
|
1045
|
+
this.child = undefined;
|
|
1046
|
+
}
|
|
1047
|
+
async waitForPendingSessionStarts() {
|
|
1048
|
+
if (this.pendingSessionStarts.size === 0) {
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
await Promise.race([
|
|
1052
|
+
Promise.allSettled([...this.pendingSessionStarts]).then(() => undefined),
|
|
1053
|
+
new Promise((resolveDelay) => {
|
|
1054
|
+
setTimeout(resolveDelay, this.runtime.shutdownGracePeriodMs);
|
|
1055
|
+
}),
|
|
1056
|
+
]);
|
|
1057
|
+
}
|
|
1058
|
+
async waitForPendingSessionCloses() {
|
|
1059
|
+
if (this.pendingSessionCloses.size === 0) {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
await Promise.race([
|
|
1063
|
+
Promise.allSettled([...this.pendingSessionCloses]).then(() => undefined),
|
|
1064
|
+
new Promise((resolveDelay) => {
|
|
1065
|
+
setTimeout(resolveDelay, this.runtime.shutdownGracePeriodMs);
|
|
1066
|
+
}),
|
|
1067
|
+
]);
|
|
1068
|
+
}
|
|
1069
|
+
async waitForChildExit(childExitPromise, timeoutMs) {
|
|
1070
|
+
let timeoutHandle;
|
|
1071
|
+
try {
|
|
1072
|
+
return await Promise.race([
|
|
1073
|
+
childExitPromise.then(() => true),
|
|
1074
|
+
new Promise((resolveDelay) => {
|
|
1075
|
+
timeoutHandle = setTimeout(() => resolveDelay(false), timeoutMs);
|
|
1076
|
+
}),
|
|
1077
|
+
]);
|
|
1078
|
+
}
|
|
1079
|
+
finally {
|
|
1080
|
+
if (timeoutHandle) {
|
|
1081
|
+
clearTimeout(timeoutHandle);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
618
1084
|
}
|
|
619
1085
|
}
|