@borgee/agents-host 0.2.2 → 0.2.26
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 +184 -21
- package/dist/agents-host-supervisor.d.ts +7 -5
- package/dist/agents-host-supervisor.js +24 -4
- package/dist/agents-host.d.ts +89 -15
- package/dist/agents-host.js +2099 -141
- package/dist/chat/chat-control-plane.d.ts +13 -2
- package/dist/chat/sdk-chat-control-plane.d.ts +14 -3
- package/dist/chat/sdk-chat-control-plane.js +54 -2
- package/dist/cli-args.d.ts +46 -5
- package/dist/cli-args.js +313 -32
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +112 -5
- package/dist/compatibility-gates.d.ts +35 -0
- package/dist/compatibility-gates.js +127 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +23 -5
- package/dist/connections-state-store.d.ts +81 -0
- package/dist/connections-state-store.js +228 -0
- package/dist/context/injection.d.ts +109 -0
- package/dist/context/injection.js +350 -0
- package/dist/context/prompt.d.ts +4 -1
- package/dist/context/prompt.js +170 -1
- package/dist/context/turn-preparation.d.ts +9 -0
- package/dist/context/turn-preparation.js +106 -0
- package/dist/debug.d.ts +44 -0
- package/dist/debug.js +135 -0
- package/dist/gateway/localhost-gateway.d.ts +52 -0
- package/dist/gateway/localhost-gateway.js +857 -0
- package/dist/index.js +7 -5
- package/dist/local-config.d.ts +4 -1
- package/dist/local-config.js +24 -7
- package/dist/managed-daemon-log.d.ts +34 -0
- package/dist/managed-daemon-log.js +261 -0
- package/dist/managed-daemon.d.ts +220 -0
- package/dist/managed-daemon.js +1601 -0
- package/dist/policy/authorization-audit.d.ts +63 -0
- package/dist/policy/authorization-audit.js +94 -0
- package/dist/policy/copilot-permission.d.ts +15 -0
- package/dist/policy/copilot-permission.js +193 -0
- package/dist/policy/gateway-authorization.d.ts +42 -0
- package/dist/policy/gateway-authorization.js +162 -0
- package/dist/providers/awaiting-user.d.ts +12 -0
- package/dist/providers/awaiting-user.js +151 -0
- package/dist/providers/claude/adapter.d.ts +3 -1
- package/dist/providers/claude/adapter.js +8 -12
- package/dist/providers/claude/cli-client.d.ts +12 -5
- package/dist/providers/claude/cli-client.js +184 -37
- package/dist/providers/claude/session-store.d.ts +1 -0
- package/dist/providers/codex/adapter.d.ts +11 -0
- package/dist/providers/codex/adapter.js +19 -0
- package/dist/providers/codex/cli-client.d.ts +103 -0
- package/dist/providers/codex/cli-client.js +1133 -0
- package/dist/providers/codex/project-doc.d.ts +3 -0
- package/dist/providers/codex/project-doc.js +66 -0
- package/dist/providers/codex/session-store.d.ts +38 -0
- package/dist/providers/codex/session-store.js +150 -0
- package/dist/providers/copilot/adapter.d.ts +3 -1
- package/dist/providers/copilot/adapter.js +8 -12
- package/dist/providers/copilot/cli-client.d.ts +20 -2
- package/dist/providers/copilot/cli-client.js +251 -71
- package/dist/providers/copilot/session-store.d.ts +1 -0
- package/dist/providers/create-provider.d.ts +11 -2
- package/dist/providers/create-provider.js +131 -12
- package/dist/run.d.ts +1 -0
- package/dist/run.js +5 -2
- package/dist/state-paths.d.ts +13 -1
- package/dist/state-paths.js +84 -3
- package/dist/task-thread-resolution.d.ts +10 -0
- package/dist/task-thread-resolution.js +48 -0
- package/dist/types.d.ts +174 -1
- package/dist/visible-mentions.d.ts +3 -0
- package/dist/visible-mentions.js +15 -0
- package/package.json +19 -17
- package/skills/borgee-agent/SKILL.md +33 -0
- package/skills/borgee-agent/borgee-agent.mjs +473 -0
- package/skills/borgee-agent/borgee-agent.py +409 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Readable, Writable } from 'node:stream';
|
|
2
2
|
import spawn from 'cross-spawn';
|
|
3
3
|
import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from '@agentclientprotocol/sdk';
|
|
4
|
+
import { HostLogger, summarizeChildStderr, summarizeError } from '../../debug.js';
|
|
5
|
+
import { resolveCopilotPermissionResponse } from '../../policy/copilot-permission.js';
|
|
4
6
|
const SESSION_TAINTED_ERRORS = new WeakSet();
|
|
5
7
|
const DEFAULT_IDLE_SESSION_TTL_MS = 2 * 24 * 60 * 60 * 1000;
|
|
6
8
|
const DEFAULT_SHUTDOWN_GRACE_PERIOD_MS = 250;
|
|
@@ -102,7 +104,7 @@ class CopilotProgressCollector {
|
|
|
102
104
|
this.onProgress({ text });
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
|
-
function createDeferredTurn(prompt, options) {
|
|
107
|
+
function createDeferredTurn(channelId, prompt, sessionPersistence, promptContext, options) {
|
|
106
108
|
let settled = false;
|
|
107
109
|
let resolvePromise;
|
|
108
110
|
let rejectPromise;
|
|
@@ -111,7 +113,10 @@ function createDeferredTurn(prompt, options) {
|
|
|
111
113
|
rejectPromise = reject;
|
|
112
114
|
});
|
|
113
115
|
return {
|
|
116
|
+
channelId,
|
|
114
117
|
prompt,
|
|
118
|
+
sessionPersistence,
|
|
119
|
+
promptContext,
|
|
115
120
|
options,
|
|
116
121
|
promise,
|
|
117
122
|
resolve(value) {
|
|
@@ -131,6 +136,34 @@ function createDeferredTurn(prompt, options) {
|
|
|
131
136
|
function normalizeError(error) {
|
|
132
137
|
return error instanceof Error ? error : new Error(String(error));
|
|
133
138
|
}
|
|
139
|
+
function parsePersistedSessionRecord(rawValue) {
|
|
140
|
+
const trimmed = rawValue.trim();
|
|
141
|
+
if (trimmed.startsWith('{')) {
|
|
142
|
+
const parsed = JSON.parse(trimmed);
|
|
143
|
+
if (typeof parsed.sessionId === 'string' && parsed.sessionId.trim().length > 0) {
|
|
144
|
+
return {
|
|
145
|
+
sessionId: parsed.sessionId,
|
|
146
|
+
cwd: typeof parsed.cwd === 'string' && parsed.cwd.trim().length > 0
|
|
147
|
+
? parsed.cwd
|
|
148
|
+
: undefined,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { sessionId: rawValue };
|
|
153
|
+
}
|
|
154
|
+
function serializePersistedSessionRecord(record) {
|
|
155
|
+
return JSON.stringify({
|
|
156
|
+
sessionId: record.sessionId,
|
|
157
|
+
...(record.cwd ? { cwd: record.cwd } : {}),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function resolveSessionRouting(turn) {
|
|
161
|
+
return {
|
|
162
|
+
channelId: turn.channelId,
|
|
163
|
+
key: turn.providerSessionRouting?.key ?? turn.channelId,
|
|
164
|
+
persistence: turn.providerSessionRouting?.persistence ?? 'persistent',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
134
167
|
function asObject(value) {
|
|
135
168
|
return typeof value === 'object' && value !== null ? value : null;
|
|
136
169
|
}
|
|
@@ -157,14 +190,6 @@ function markSessionTainted(error) {
|
|
|
157
190
|
function isSessionTainted(error) {
|
|
158
191
|
return error instanceof Error && SESSION_TAINTED_ERRORS.has(error);
|
|
159
192
|
}
|
|
160
|
-
function selectPermissionOption(options) {
|
|
161
|
-
const option = options.find((candidate) => candidate.kind === 'allow_always') ??
|
|
162
|
-
options.find((candidate) => candidate.kind === 'allow_once');
|
|
163
|
-
if (!option) {
|
|
164
|
-
throw new Error('Copilot ACP requested permission but did not offer an allow option');
|
|
165
|
-
}
|
|
166
|
-
return option.optionId;
|
|
167
|
-
}
|
|
168
193
|
/**
|
|
169
194
|
* Persistent ACP-backed client for the GitHub Copilot CLI (`copilot --acp`).
|
|
170
195
|
*
|
|
@@ -177,12 +202,15 @@ export class CopilotCliClient {
|
|
|
177
202
|
command;
|
|
178
203
|
sessionStore;
|
|
179
204
|
resolveSessionStoreAgentId;
|
|
205
|
+
logger;
|
|
206
|
+
permissionPolicy;
|
|
180
207
|
runtime;
|
|
181
208
|
channels = new Map();
|
|
182
209
|
persistedSessions = new Map();
|
|
183
210
|
closingSessions = new WeakSet();
|
|
184
211
|
pendingSessionStarts = new Set();
|
|
185
212
|
pendingSessionCloses = new Set();
|
|
213
|
+
pendingSessionStoreOperations = new Set();
|
|
186
214
|
fatalPromise;
|
|
187
215
|
rejectFatalPromise;
|
|
188
216
|
child;
|
|
@@ -198,33 +226,63 @@ export class CopilotCliClient {
|
|
|
198
226
|
sessionStoreLoadPromise = null;
|
|
199
227
|
sessionStoreWriteQueue = Promise.resolve();
|
|
200
228
|
sessionCapabilities = DEFAULT_SESSION_CAPABILITIES;
|
|
201
|
-
constructor(command, _ignoredArgs = [], runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined) {
|
|
229
|
+
constructor(command, _ignoredArgs = [], runtimeOverrides = {}, sessionStore, resolveSessionStoreAgentId = () => undefined, logger = new HostLogger(), permissionPolicy = {}) {
|
|
202
230
|
this.command = command;
|
|
203
231
|
this.sessionStore = sessionStore;
|
|
204
232
|
this.resolveSessionStoreAgentId = resolveSessionStoreAgentId;
|
|
233
|
+
this.logger = logger;
|
|
234
|
+
this.permissionPolicy = permissionPolicy;
|
|
205
235
|
this.runtime = { ...DEFAULT_RUNTIME, ...runtimeOverrides };
|
|
206
236
|
this.fatalPromise = new Promise((_, reject) => {
|
|
207
237
|
this.rejectFatalPromise = reject;
|
|
208
238
|
});
|
|
209
239
|
void this.fatalPromise.catch(() => { });
|
|
210
240
|
}
|
|
211
|
-
async generateReply(
|
|
241
|
+
async generateReply(channelIdOrTurn, promptOrOptions, maybeOptions) {
|
|
212
242
|
if (this.fatalError) {
|
|
213
243
|
throw this.fatalError;
|
|
214
244
|
}
|
|
215
|
-
const
|
|
245
|
+
const preparedTurn = typeof channelIdOrTurn === 'string'
|
|
246
|
+
? {
|
|
247
|
+
channelId: channelIdOrTurn,
|
|
248
|
+
prompt: typeof promptOrOptions === 'string' ? promptOrOptions : '',
|
|
249
|
+
}
|
|
250
|
+
: channelIdOrTurn;
|
|
251
|
+
const options = typeof channelIdOrTurn === 'string'
|
|
252
|
+
? maybeOptions
|
|
253
|
+
: promptOrOptions;
|
|
254
|
+
const sessionRouting = resolveSessionRouting(preparedTurn);
|
|
255
|
+
const state = this.getOrCreateChannelState(sessionRouting.key, preparedTurn.channelId, sessionRouting.persistence);
|
|
216
256
|
this.clearIdleTimer(state);
|
|
217
|
-
const turn = createDeferredTurn(prompt, options);
|
|
257
|
+
const turn = createDeferredTurn(preparedTurn.channelId, preparedTurn.prompt, sessionRouting.persistence, preparedTurn.promptContext, options);
|
|
218
258
|
state.queue.push(turn);
|
|
219
|
-
this.processChannelQueue(
|
|
259
|
+
this.processChannelQueue(sessionRouting.key, state);
|
|
220
260
|
return turn.promise;
|
|
221
261
|
}
|
|
222
262
|
async dispose() {
|
|
223
263
|
const error = new Error('Copilot ACP backend stopped');
|
|
224
264
|
this.disposing = true;
|
|
265
|
+
this.logger?.debug('stopping Copilot ACP backend');
|
|
225
266
|
const closed = this.connection?.closed ?? Promise.resolve();
|
|
226
267
|
this.failAll(error);
|
|
227
|
-
|
|
268
|
+
let thrown;
|
|
269
|
+
try {
|
|
270
|
+
await Promise.all([closed, this.shutdownPromise ?? Promise.resolve()]);
|
|
271
|
+
}
|
|
272
|
+
catch (disposeError) {
|
|
273
|
+
thrown = disposeError;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
await Promise.allSettled(this.pendingSessionStoreOperations);
|
|
277
|
+
await this.sessionStoreWriteQueue;
|
|
278
|
+
await this.sessionStore?.close?.();
|
|
279
|
+
}
|
|
280
|
+
catch (closeError) {
|
|
281
|
+
thrown ??= closeError;
|
|
282
|
+
}
|
|
283
|
+
if (thrown) {
|
|
284
|
+
throw thrown;
|
|
285
|
+
}
|
|
228
286
|
}
|
|
229
287
|
async ensureStarted() {
|
|
230
288
|
if (this.fatalError) {
|
|
@@ -242,18 +300,32 @@ export class CopilotCliClient {
|
|
|
242
300
|
}
|
|
243
301
|
}
|
|
244
302
|
async startBackend() {
|
|
303
|
+
this.logger?.debug('starting Copilot ACP process', {
|
|
304
|
+
command: this.command,
|
|
305
|
+
args: ['--acp'],
|
|
306
|
+
cwd: this.runtime.cwd,
|
|
307
|
+
});
|
|
245
308
|
const child = this.runtime.spawn(this.command, ['--acp'], {
|
|
246
309
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
247
310
|
});
|
|
248
311
|
this.child = child;
|
|
312
|
+
child.stderr.setEncoding('utf8');
|
|
313
|
+
child.stderr.on('data', (chunk) => {
|
|
314
|
+
this.logger?.childStderr('copilot stderr', summarizeChildStderr(chunk));
|
|
315
|
+
});
|
|
249
316
|
child.stderr.resume();
|
|
317
|
+
this.logger?.debug('spawned Copilot ACP process', { pid: child.pid });
|
|
250
318
|
child.once('error', (error) => {
|
|
319
|
+
this.logger?.debugError('Copilot ACP process error', error);
|
|
251
320
|
this.failAll(new Error(`Copilot ACP process error: ${normalizeError(error).message}`));
|
|
252
321
|
});
|
|
253
322
|
child.once('exit', (code, signal) => {
|
|
254
323
|
this.resolveChildExit?.();
|
|
255
|
-
if (this.disposing)
|
|
324
|
+
if (this.disposing) {
|
|
325
|
+
this.logger?.debug('Copilot ACP process exited during shutdown', { code, signal });
|
|
256
326
|
return;
|
|
327
|
+
}
|
|
328
|
+
this.logger?.debugError('Copilot ACP process exited unexpectedly', { code, signal });
|
|
257
329
|
const suffix = signal ? `signal ${signal}` : `code ${String(code ?? 'unknown')}`;
|
|
258
330
|
this.failAll(new Error(`Copilot ACP process exited unexpectedly (${suffix})`));
|
|
259
331
|
});
|
|
@@ -265,16 +337,12 @@ export class CopilotCliClient {
|
|
|
265
337
|
const stream = this.runtime.ndJsonStream(output, input);
|
|
266
338
|
const app = this.runtime
|
|
267
339
|
.client({ name: 'borgee-agents-host' })
|
|
268
|
-
.onRequest(this.runtime.methods.client.session.requestPermission, ({ params }) => (
|
|
269
|
-
outcome: {
|
|
270
|
-
outcome: 'selected',
|
|
271
|
-
optionId: selectPermissionOption(params.options),
|
|
272
|
-
},
|
|
273
|
-
}));
|
|
340
|
+
.onRequest(this.runtime.methods.client.session.requestPermission, ({ params }) => (this.handlePermissionRequest(params)));
|
|
274
341
|
const connection = app.connect(stream);
|
|
275
342
|
this.connection = connection;
|
|
276
343
|
void connection.closed.then(() => {
|
|
277
344
|
if (!this.disposing) {
|
|
345
|
+
this.logger?.debugError('Copilot ACP connection closed unexpectedly');
|
|
278
346
|
this.failAll(new Error('Copilot ACP connection closed unexpectedly'));
|
|
279
347
|
}
|
|
280
348
|
});
|
|
@@ -288,23 +356,34 @@ export class CopilotCliClient {
|
|
|
288
356
|
},
|
|
289
357
|
});
|
|
290
358
|
this.sessionCapabilities = readSessionCapabilities(initializeResponse);
|
|
359
|
+
this.logger?.debug('initialized Copilot ACP connection', {
|
|
360
|
+
loadSession: this.sessionCapabilities.loadSession,
|
|
361
|
+
resumeSession: this.sessionCapabilities.resumeSession,
|
|
362
|
+
});
|
|
291
363
|
}
|
|
292
364
|
catch (error) {
|
|
293
365
|
const normalized = new Error(`Copilot ACP initialize failed: ${normalizeError(error).message}`);
|
|
366
|
+
this.logger?.debugError('Copilot ACP initialize failed', normalized);
|
|
294
367
|
this.failAll(normalized);
|
|
295
368
|
throw normalized;
|
|
296
369
|
}
|
|
297
370
|
}
|
|
298
|
-
getOrCreateChannelState(channelId) {
|
|
371
|
+
getOrCreateChannelState(channelId, resolvedChannelId, sessionPersistence) {
|
|
299
372
|
let state = this.channels.get(channelId);
|
|
300
373
|
if (!state) {
|
|
301
374
|
state = {
|
|
375
|
+
channelId: resolvedChannelId,
|
|
376
|
+
sessionPersistence,
|
|
302
377
|
idleTimerGeneration: 0,
|
|
303
378
|
processing: false,
|
|
304
379
|
queue: [],
|
|
305
380
|
};
|
|
306
381
|
this.channels.set(channelId, state);
|
|
307
382
|
}
|
|
383
|
+
else if (state.channelId !== resolvedChannelId
|
|
384
|
+
|| state.sessionPersistence !== sessionPersistence) {
|
|
385
|
+
throw new Error(`Copilot session routing changed for key "${channelId}"`);
|
|
386
|
+
}
|
|
308
387
|
return state;
|
|
309
388
|
}
|
|
310
389
|
processChannelQueue(channelId, state) {
|
|
@@ -321,7 +400,9 @@ export class CopilotCliClient {
|
|
|
321
400
|
}
|
|
322
401
|
state.activeTurn = turn;
|
|
323
402
|
try {
|
|
403
|
+
state.cwd = await this.resolveSessionCwd(turn.promptContext);
|
|
324
404
|
await this.ensureStarted();
|
|
405
|
+
await this.recycleSessionIfCwdChanged(channelId, state);
|
|
325
406
|
const session = await this.getOrCreateSession(channelId, state);
|
|
326
407
|
let reply;
|
|
327
408
|
try {
|
|
@@ -329,7 +410,7 @@ export class CopilotCliClient {
|
|
|
329
410
|
}
|
|
330
411
|
catch (error) {
|
|
331
412
|
if (isSessionTainted(error)) {
|
|
332
|
-
this.invalidateSession(state, session);
|
|
413
|
+
this.invalidateSession(channelId, state, session);
|
|
333
414
|
this.rejectQueuedTurnsAfterSessionTaint(state, error);
|
|
334
415
|
}
|
|
335
416
|
throw error;
|
|
@@ -360,11 +441,13 @@ export class CopilotCliClient {
|
|
|
360
441
|
if (!this.connection) {
|
|
361
442
|
throw new Error('Copilot ACP connection is not available');
|
|
362
443
|
}
|
|
363
|
-
|
|
364
|
-
const persistedSessionId =
|
|
444
|
+
const cwd = state.cwd ?? this.runtime.cwd;
|
|
445
|
+
const persistedSessionId = state.sessionPersistence === 'persistent'
|
|
446
|
+
? await this.readPersistedSessionId(channelId, cwd)
|
|
447
|
+
: undefined;
|
|
365
448
|
const sessionPromise = persistedSessionId
|
|
366
|
-
? this.restoreOrCreateSession(channelId, persistedSessionId)
|
|
367
|
-
: this.startFreshSession(channelId);
|
|
449
|
+
? this.restoreOrCreateSession(channelId, persistedSessionId, cwd)
|
|
450
|
+
: this.startFreshSession(channelId, cwd);
|
|
368
451
|
this.pendingSessionStarts.add(sessionPromise);
|
|
369
452
|
state.sessionPromise = sessionPromise;
|
|
370
453
|
let sessionAdopted = false;
|
|
@@ -387,10 +470,12 @@ export class CopilotCliClient {
|
|
|
387
470
|
}
|
|
388
471
|
sessionAdopted = true;
|
|
389
472
|
state.session = session;
|
|
473
|
+
state.sessionCwd = cwd;
|
|
390
474
|
return session;
|
|
391
475
|
}
|
|
392
476
|
catch (error) {
|
|
393
477
|
state.session = undefined;
|
|
478
|
+
state.sessionCwd = undefined;
|
|
394
479
|
throw error;
|
|
395
480
|
}
|
|
396
481
|
finally {
|
|
@@ -399,30 +484,41 @@ export class CopilotCliClient {
|
|
|
399
484
|
}
|
|
400
485
|
}
|
|
401
486
|
}
|
|
402
|
-
async startFreshSession(channelId) {
|
|
487
|
+
async startFreshSession(channelId, cwd) {
|
|
403
488
|
if (!this.connection) {
|
|
404
489
|
throw new Error('Copilot ACP connection is not available');
|
|
405
490
|
}
|
|
406
|
-
const session = await this.connection.agent.buildSession(
|
|
407
|
-
|
|
491
|
+
const session = await this.connection.agent.buildSession(cwd).start();
|
|
492
|
+
this.logger?.debug('started fresh Copilot ACP session', { channelId });
|
|
493
|
+
const state = this.channels.get(channelId);
|
|
494
|
+
if (state?.sessionPersistence === 'persistent') {
|
|
495
|
+
await this.persistSessionBestEffort(channelId, session.sessionId, cwd);
|
|
496
|
+
}
|
|
408
497
|
return session;
|
|
409
498
|
}
|
|
410
|
-
async restoreOrCreateSession(channelId, sessionId) {
|
|
499
|
+
async restoreOrCreateSession(channelId, sessionId, cwd) {
|
|
411
500
|
if (!this.sessionCapabilities.resumeSession && !this.sessionCapabilities.loadSession) {
|
|
412
|
-
|
|
501
|
+
this.logger?.debug('Copilot ACP restore unsupported; starting fresh session', { channelId });
|
|
502
|
+
return this.startFreshSession(channelId, cwd);
|
|
413
503
|
}
|
|
414
504
|
try {
|
|
415
|
-
|
|
505
|
+
const session = await this.restoreSession(sessionId, cwd);
|
|
506
|
+
this.logger?.debug('restored Copilot ACP session', {
|
|
507
|
+
channelId,
|
|
508
|
+
restoreMethod: this.sessionCapabilities.resumeSession ? 'session/resume' : 'session/load',
|
|
509
|
+
});
|
|
510
|
+
return session;
|
|
416
511
|
}
|
|
417
512
|
catch (error) {
|
|
418
513
|
if (!isStaleRestoreFailure(error)) {
|
|
419
514
|
throw normalizeError(error);
|
|
420
515
|
}
|
|
516
|
+
this.logger?.debug('discarded stale Copilot ACP session and started fresh', { channelId });
|
|
421
517
|
await this.clearPersistedSessionBestEffort(channelId);
|
|
422
|
-
return this.startFreshSession(channelId);
|
|
518
|
+
return this.startFreshSession(channelId, cwd);
|
|
423
519
|
}
|
|
424
520
|
}
|
|
425
|
-
async restoreSession(sessionId) {
|
|
521
|
+
async restoreSession(sessionId, cwd) {
|
|
426
522
|
if (!this.connection) {
|
|
427
523
|
throw new Error('Copilot ACP connection is not available');
|
|
428
524
|
}
|
|
@@ -435,7 +531,7 @@ export class CopilotCliClient {
|
|
|
435
531
|
if (this.sessionCapabilities.resumeSession) {
|
|
436
532
|
await this.connection.agent.request(this.runtime.methods.agent.session.resume, {
|
|
437
533
|
sessionId,
|
|
438
|
-
cwd
|
|
534
|
+
cwd,
|
|
439
535
|
mcpServers: [],
|
|
440
536
|
});
|
|
441
537
|
return session;
|
|
@@ -443,7 +539,7 @@ export class CopilotCliClient {
|
|
|
443
539
|
if (this.sessionCapabilities.loadSession) {
|
|
444
540
|
await this.connection.agent.request(this.runtime.methods.agent.session.load, {
|
|
445
541
|
sessionId,
|
|
446
|
-
cwd
|
|
542
|
+
cwd,
|
|
447
543
|
mcpServers: [],
|
|
448
544
|
});
|
|
449
545
|
this.clearBufferedSessionReplay(session);
|
|
@@ -462,6 +558,31 @@ export class CopilotCliClient {
|
|
|
462
558
|
updates.values = [];
|
|
463
559
|
}
|
|
464
560
|
}
|
|
561
|
+
async resolveSessionCwd(promptContext) {
|
|
562
|
+
return promptContext?.taskWorkspace?.rootPath ?? this.runtime.cwd;
|
|
563
|
+
}
|
|
564
|
+
async recycleSessionIfCwdChanged(channelId, state) {
|
|
565
|
+
const session = state.session;
|
|
566
|
+
if (!session) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const nextCwd = state.cwd ?? this.runtime.cwd;
|
|
570
|
+
if (state.sessionCwd === nextCwd) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
this.logger?.debug('recycling Copilot ACP session after cwd changed', {
|
|
574
|
+
channelId,
|
|
575
|
+
previousCwd: state.sessionCwd,
|
|
576
|
+
nextCwd,
|
|
577
|
+
});
|
|
578
|
+
state.session = undefined;
|
|
579
|
+
state.sessionCwd = undefined;
|
|
580
|
+
this.clearIdleTimer(state);
|
|
581
|
+
if (!this.disposing && !this.backendClosed && state.sessionPersistence === 'persistent') {
|
|
582
|
+
await this.clearPersistedSessionBestEffort(channelId);
|
|
583
|
+
}
|
|
584
|
+
this.closeSession(session);
|
|
585
|
+
}
|
|
465
586
|
async runTurn(session, prompt, options) {
|
|
466
587
|
const promptPromise = this.raceWithFatal(session.prompt(prompt));
|
|
467
588
|
const promptFailure = new Promise((_, reject) => {
|
|
@@ -505,6 +626,17 @@ export class CopilotCliClient {
|
|
|
505
626
|
}
|
|
506
627
|
return Promise.race([promise, this.fatalPromise]);
|
|
507
628
|
}
|
|
629
|
+
handlePermissionRequest(params) {
|
|
630
|
+
return resolveCopilotPermissionResponse({
|
|
631
|
+
gateEnabled: this.permissionPolicy.gateEnabled ?? false,
|
|
632
|
+
policyMode: this.permissionPolicy.policyMode ?? 'audit-only',
|
|
633
|
+
params,
|
|
634
|
+
logger: this.logger,
|
|
635
|
+
auditSink: this.permissionPolicy.auditSink,
|
|
636
|
+
agentId: this.resolveSessionStoreAgentId()?.trim() || undefined,
|
|
637
|
+
channelId: this.findChannelIdBySessionId(params.sessionId),
|
|
638
|
+
});
|
|
639
|
+
}
|
|
508
640
|
failAll(error) {
|
|
509
641
|
if (this.fatalError) {
|
|
510
642
|
return;
|
|
@@ -522,6 +654,7 @@ export class CopilotCliClient {
|
|
|
522
654
|
this.closeSession(state.session);
|
|
523
655
|
}
|
|
524
656
|
state.session = undefined;
|
|
657
|
+
state.sessionCwd = undefined;
|
|
525
658
|
state.sessionPromise = undefined;
|
|
526
659
|
state.activeTurn = undefined;
|
|
527
660
|
if (!state.processing) {
|
|
@@ -531,17 +664,16 @@ export class CopilotCliClient {
|
|
|
531
664
|
this.channels.clear();
|
|
532
665
|
this.shutdownPromise = this.shutdownBackend(error);
|
|
533
666
|
}
|
|
534
|
-
invalidateSession(state, session) {
|
|
667
|
+
invalidateSession(channelId, state, session) {
|
|
535
668
|
this.clearIdleTimer(state);
|
|
536
669
|
if (state.session === session) {
|
|
537
670
|
state.session = undefined;
|
|
671
|
+
state.sessionCwd = undefined;
|
|
538
672
|
}
|
|
673
|
+
this.logger?.debug('discarding tainted Copilot ACP session', { channelId });
|
|
539
674
|
this.closeSession(session);
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
void this.clearPersistedSessionBestEffort(channelId);
|
|
543
|
-
break;
|
|
544
|
-
}
|
|
675
|
+
if (!this.disposing && !this.backendClosed && state.sessionPersistence === 'persistent') {
|
|
676
|
+
void this.clearPersistedSessionBestEffort(channelId);
|
|
545
677
|
}
|
|
546
678
|
}
|
|
547
679
|
closeSession(session) {
|
|
@@ -589,6 +721,14 @@ export class CopilotCliClient {
|
|
|
589
721
|
clearTimeout(state.idleTimer);
|
|
590
722
|
state.idleTimer = undefined;
|
|
591
723
|
}
|
|
724
|
+
findChannelIdBySessionId(sessionId) {
|
|
725
|
+
for (const state of this.channels.values()) {
|
|
726
|
+
if (state.session?.sessionId === sessionId) {
|
|
727
|
+
return state.channelId;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return undefined;
|
|
731
|
+
}
|
|
592
732
|
reconcileIdleChannelState(channelId, state) {
|
|
593
733
|
if (this.channels.get(channelId) !== state) {
|
|
594
734
|
return;
|
|
@@ -606,6 +746,15 @@ export class CopilotCliClient {
|
|
|
606
746
|
this.channels.delete(channelId);
|
|
607
747
|
return;
|
|
608
748
|
}
|
|
749
|
+
if (state.sessionPersistence === 'ephemeral') {
|
|
750
|
+
this.clearIdleTimer(state);
|
|
751
|
+
const session = state.session;
|
|
752
|
+
state.session = undefined;
|
|
753
|
+
state.sessionCwd = undefined;
|
|
754
|
+
this.closeSession(session);
|
|
755
|
+
this.channels.delete(channelId);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
609
758
|
this.clearIdleTimer(state);
|
|
610
759
|
const generation = state.idleTimerGeneration;
|
|
611
760
|
state.idleTimer = setTimeout(() => {
|
|
@@ -627,8 +776,8 @@ export class CopilotCliClient {
|
|
|
627
776
|
this.sessionStoreLoadPromise = (async () => {
|
|
628
777
|
const loaded = await this.sessionStore.load(agentId);
|
|
629
778
|
this.persistedSessions.clear();
|
|
630
|
-
for (const [channelId,
|
|
631
|
-
this.persistedSessions.set(channelId,
|
|
779
|
+
for (const [channelId, sessionValue] of Object.entries(loaded)) {
|
|
780
|
+
this.persistedSessions.set(channelId, parsePersistedSessionRecord(sessionValue));
|
|
632
781
|
}
|
|
633
782
|
this.loadedSessionStoreAgentId = agentId;
|
|
634
783
|
})().finally(() => {
|
|
@@ -637,23 +786,37 @@ export class CopilotCliClient {
|
|
|
637
786
|
}
|
|
638
787
|
await this.sessionStoreLoadPromise;
|
|
639
788
|
}
|
|
640
|
-
async
|
|
789
|
+
async readPersistedSessionId(channelId, cwd) {
|
|
641
790
|
await this.ensureSessionStoreLoaded();
|
|
642
|
-
this.persistedSessions.
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
async persistSessionBestEffort(channelId, sessionId) {
|
|
646
|
-
try {
|
|
647
|
-
await this.persistSession(channelId, sessionId);
|
|
791
|
+
const record = this.persistedSessions.get(channelId);
|
|
792
|
+
if (!record) {
|
|
793
|
+
return undefined;
|
|
648
794
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
channelId,
|
|
653
|
-
sessionId,
|
|
654
|
-
error,
|
|
655
|
-
});
|
|
795
|
+
if (!record.cwd || record.cwd !== cwd) {
|
|
796
|
+
await this.clearPersistedSessionBestEffort(channelId);
|
|
797
|
+
return undefined;
|
|
656
798
|
}
|
|
799
|
+
return record.sessionId;
|
|
800
|
+
}
|
|
801
|
+
async persistSession(channelId, sessionId, cwd) {
|
|
802
|
+
await this.ensureSessionStoreLoaded();
|
|
803
|
+
this.persistedSessions.set(channelId, { sessionId, cwd });
|
|
804
|
+
await this.flushSessionStore();
|
|
805
|
+
}
|
|
806
|
+
async persistSessionBestEffort(channelId, sessionId, cwd) {
|
|
807
|
+
await this.trackSessionStoreOperation((async () => {
|
|
808
|
+
try {
|
|
809
|
+
await this.persistSession(channelId, sessionId, cwd);
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
this.logger.error('failed to persist Copilot session map; keeping reply delivery', {
|
|
813
|
+
agentId: this.loadedSessionStoreAgentId,
|
|
814
|
+
channelId,
|
|
815
|
+
sessionId,
|
|
816
|
+
error: summarizeError(error),
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
})());
|
|
657
820
|
}
|
|
658
821
|
async clearPersistedSession(channelId) {
|
|
659
822
|
await this.ensureSessionStoreLoaded();
|
|
@@ -663,27 +826,39 @@ export class CopilotCliClient {
|
|
|
663
826
|
await this.flushSessionStore();
|
|
664
827
|
}
|
|
665
828
|
async clearPersistedSessionBestEffort(channelId) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
829
|
+
await this.trackSessionStoreOperation((async () => {
|
|
830
|
+
try {
|
|
831
|
+
await this.clearPersistedSession(channelId);
|
|
832
|
+
}
|
|
833
|
+
catch (error) {
|
|
834
|
+
this.logger.error('failed to clear Copilot session map; retrying in-memory only', {
|
|
835
|
+
agentId: this.loadedSessionStoreAgentId,
|
|
836
|
+
channelId,
|
|
837
|
+
error: summarizeError(error),
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
})());
|
|
676
841
|
}
|
|
677
842
|
async flushSessionStore() {
|
|
678
843
|
if (!this.sessionStore || !this.loadedSessionStoreAgentId) {
|
|
679
844
|
return;
|
|
680
845
|
}
|
|
681
846
|
const agentId = this.loadedSessionStoreAgentId;
|
|
682
|
-
const snapshot = Object.fromEntries(this.persistedSessions.entries())
|
|
847
|
+
const snapshot = Object.fromEntries([...this.persistedSessions.entries()].map(([channelId, record]) => [
|
|
848
|
+
channelId,
|
|
849
|
+
serializePersistedSessionRecord(record),
|
|
850
|
+
]));
|
|
683
851
|
const write = this.sessionStoreWriteQueue.then(() => this.sessionStore.save(agentId, snapshot));
|
|
684
852
|
this.sessionStoreWriteQueue = write.catch(() => { });
|
|
685
853
|
await write;
|
|
686
854
|
}
|
|
855
|
+
async trackSessionStoreOperation(operation) {
|
|
856
|
+
const tracked = operation.finally(() => {
|
|
857
|
+
this.pendingSessionStoreOperations.delete(tracked);
|
|
858
|
+
});
|
|
859
|
+
this.pendingSessionStoreOperations.add(tracked);
|
|
860
|
+
return tracked;
|
|
861
|
+
}
|
|
687
862
|
evictIdleChannel(channelId, state, generation) {
|
|
688
863
|
if (this.disposing || this.fatalError || this.backendClosed) {
|
|
689
864
|
return;
|
|
@@ -697,7 +872,9 @@ export class CopilotCliClient {
|
|
|
697
872
|
}
|
|
698
873
|
const session = state.session;
|
|
699
874
|
state.session = undefined;
|
|
875
|
+
state.sessionCwd = undefined;
|
|
700
876
|
if (session) {
|
|
877
|
+
this.logger?.debug('evicting idle Copilot ACP session', { channelId });
|
|
701
878
|
this.closeSession(session);
|
|
702
879
|
}
|
|
703
880
|
this.channels.delete(channelId);
|
|
@@ -718,12 +895,15 @@ export class CopilotCliClient {
|
|
|
718
895
|
return;
|
|
719
896
|
}
|
|
720
897
|
const childExitPromise = this.childExitPromise ?? Promise.resolve();
|
|
898
|
+
this.logger?.debug('sending SIGTERM to Copilot ACP process');
|
|
721
899
|
child.kill('SIGTERM');
|
|
722
900
|
const exitedAfterTerm = await this.waitForChildExit(childExitPromise, this.runtime.shutdownGracePeriodMs);
|
|
723
901
|
if (exitedAfterTerm) {
|
|
902
|
+
this.logger?.debug('Copilot ACP process exited after SIGTERM');
|
|
724
903
|
this.child = undefined;
|
|
725
904
|
return;
|
|
726
905
|
}
|
|
906
|
+
this.logger?.debug('sending SIGKILL to Copilot ACP process after SIGTERM grace timeout');
|
|
727
907
|
child.kill('SIGKILL');
|
|
728
908
|
await this.waitForChildExit(childExitPromise, this.runtime.shutdownForceKillWaitMs);
|
|
729
909
|
this.child = undefined;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export interface CopilotChannelSessionStore {
|
|
2
2
|
load(agentId: string): Promise<Record<string, string>>;
|
|
3
3
|
save(agentId: string, sessions: Record<string, string>): Promise<void>;
|
|
4
|
+
close?(): Promise<void> | void;
|
|
4
5
|
}
|
|
5
6
|
export interface FileCopilotChannelSessionStoreOptions {
|
|
6
7
|
resolvePath(agentId: string): string;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { DebugLogger } from '../debug.js';
|
|
2
2
|
import type { ProviderRuntimeConfig } from '../types.js';
|
|
3
|
-
|
|
3
|
+
import type { ProviderAdapter } from './provider-adapter.js';
|
|
4
|
+
import type { LocalhostGatewayContextPublisher } from '../context/injection.js';
|
|
5
|
+
import type { AuthorizationAuditSinkLike } from '../policy/authorization-audit.js';
|
|
6
|
+
interface CreateProviderOptions {
|
|
7
|
+
compatibilityGates?: ReadonlySet<string>;
|
|
8
|
+
localhostGateway?: LocalhostGatewayContextPublisher;
|
|
9
|
+
authorizationAuditSink?: AuthorizationAuditSinkLike;
|
|
10
|
+
}
|
|
11
|
+
export declare function createProvider(config: ProviderRuntimeConfig, debugLogger?: DebugLogger, options?: CreateProviderOptions): ProviderAdapter;
|
|
12
|
+
export {};
|