@agent-link/agent 0.1.337 → 0.1.339

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.
Files changed (80) hide show
  1. package/dist/backends/types.d.ts +58 -0
  2. package/dist/backends/types.js +15 -4
  3. package/dist/backends/types.js.map +1 -1
  4. package/dist/connection.js +231 -28
  5. package/dist/connection.js.map +1 -1
  6. package/dist/copilot-capabilities-offline.d.ts +254 -0
  7. package/dist/copilot-capabilities-offline.js +392 -0
  8. package/dist/copilot-capabilities-offline.js.map +1 -0
  9. package/dist/copilot-capabilities.d.ts +43 -0
  10. package/dist/copilot-capabilities.js +128 -0
  11. package/dist/copilot-capabilities.js.map +1 -0
  12. package/dist/copilot-capability-service.d.ts +24 -0
  13. package/dist/copilot-capability-service.js +116 -0
  14. package/dist/copilot-capability-service.js.map +1 -0
  15. package/dist/copilot-command-bridge.d.ts +184 -0
  16. package/dist/copilot-command-bridge.js +1800 -0
  17. package/dist/copilot-command-bridge.js.map +1 -0
  18. package/dist/copilot-compaction-barrier.d.ts +36 -0
  19. package/dist/copilot-compaction-barrier.js +152 -0
  20. package/dist/copilot-compaction-barrier.js.map +1 -0
  21. package/dist/copilot-compaction-lifecycle.d.ts +34 -0
  22. package/dist/copilot-compaction-lifecycle.js +88 -0
  23. package/dist/copilot-compaction-lifecycle.js.map +1 -0
  24. package/dist/copilot-compaction-policy.d.ts +5 -0
  25. package/dist/copilot-compaction-policy.js +7 -0
  26. package/dist/copilot-compaction-policy.js.map +1 -0
  27. package/dist/copilot-configuration.d.ts +63 -0
  28. package/dist/copilot-configuration.js +283 -0
  29. package/dist/copilot-configuration.js.map +1 -0
  30. package/dist/copilot-event-scope.d.ts +13 -0
  31. package/dist/copilot-event-scope.js +26 -0
  32. package/dist/copilot-event-scope.js.map +1 -0
  33. package/dist/copilot-fork-metadata.d.ts +9 -0
  34. package/dist/copilot-fork-metadata.js +86 -0
  35. package/dist/copilot-fork-metadata.js.map +1 -0
  36. package/dist/copilot-message-kind.d.ts +21 -0
  37. package/dist/copilot-message-kind.js +45 -0
  38. package/dist/copilot-message-kind.js.map +1 -0
  39. package/dist/copilot-plan-state.d.ts +19 -0
  40. package/dist/copilot-plan-state.js +180 -0
  41. package/dist/copilot-plan-state.js.map +1 -0
  42. package/dist/copilot-request-dispatch.d.ts +10 -0
  43. package/dist/copilot-request-dispatch.js +31 -0
  44. package/dist/copilot-request-dispatch.js.map +1 -0
  45. package/dist/copilot-review-documents.d.ts +17 -0
  46. package/dist/copilot-review-documents.js +162 -0
  47. package/dist/copilot-review-documents.js.map +1 -0
  48. package/dist/copilot-session-activity.d.ts +3 -0
  49. package/dist/copilot-session-activity.js +94 -0
  50. package/dist/copilot-session-activity.js.map +1 -0
  51. package/dist/copilot-sidecar.d.ts +2 -0
  52. package/dist/copilot-sidecar.js +17 -0
  53. package/dist/copilot-sidecar.js.map +1 -0
  54. package/dist/copilot-turn-journal.d.ts +36 -0
  55. package/dist/copilot-turn-journal.js +246 -0
  56. package/dist/copilot-turn-journal.js.map +1 -0
  57. package/dist/copilot-user-requests.d.ts +11 -0
  58. package/dist/copilot-user-requests.js +84 -0
  59. package/dist/copilot-user-requests.js.map +1 -0
  60. package/dist/copilot.d.ts +194 -9
  61. package/dist/copilot.js +5012 -240
  62. package/dist/copilot.js.map +1 -1
  63. package/dist/directory-handlers.js +36 -8
  64. package/dist/directory-handlers.js.map +1 -1
  65. package/dist/history-message-transform.d.ts +47 -1
  66. package/dist/history-message-transform.js +67 -0
  67. package/dist/history-message-transform.js.map +1 -1
  68. package/dist/history.d.ts +8 -0
  69. package/dist/history.js.map +1 -1
  70. package/dist/provider-command-discovery.d.ts +5 -0
  71. package/dist/provider-command-discovery.js +49 -1
  72. package/dist/provider-command-discovery.js.map +1 -1
  73. package/dist/provider-command-types.d.ts +5 -0
  74. package/dist/provider-command-types.js.map +1 -1
  75. package/dist/runtime.d.ts +1 -1
  76. package/dist/runtime.js +1 -1
  77. package/dist/runtime.js.map +1 -1
  78. package/dist/search-sessions.js +2 -1
  79. package/dist/search-sessions.js.map +1 -1
  80. package/package.json +3 -1
@@ -0,0 +1,1800 @@
1
+ import { execFile, spawn } from 'child_process';
2
+ import { COPILOT_COMPACTION_POLICY } from './copilot-compaction-policy.js';
3
+ import { createHash, randomUUID } from 'crypto';
4
+ import { existsSync, readFileSync, statSync } from 'fs';
5
+ import { dirname, isAbsolute, join, resolve, sep } from 'path';
6
+ import { homedir, platform } from 'os';
7
+ import { Readable, Writable } from 'stream';
8
+ import * as acp from '@agentclientprotocol/sdk';
9
+ import { createMessageConnection, StreamMessageReader, StreamMessageWriter, } from 'vscode-jsonrpc/node.js';
10
+ import { getProvider, resolveProviderCommand } from './providers.js';
11
+ import { getCleanEnv, resolveExecutablePath } from './sdk.js';
12
+ import { CopilotTurnJournal } from './copilot-turn-journal.js';
13
+ import { CopilotCompactionLifecycle } from './copilot-compaction-lifecycle.js';
14
+ import { copilotChildScope, isCopilotChildEvent } from './copilot-event-scope.js';
15
+ import { applyModelCatalog, configurationIssue, confirmConfiguration, createCopilotConfiguration, finishConfiguration, observeConfigurationEvent, readConfigurationEvents, resetCopilotConfigurationObservation } from './copilot-configuration.js';
16
+ import { beginCopilotPlanState, copilotPlanControlPrompt, readCopilotPlanState, sealCopilotPlanState, stripCopilotPlanControl } from './copilot-plan-state.js';
17
+ import { assertCopilotCapabilities, hasCopilotOfflineModel, mergeCopilotCapabilities } from './copilot-capabilities.js';
18
+ const SDK_START_TIMEOUT_MS = 30_000;
19
+ const AGENCY_ACP_START_TIMEOUT_MS = 5 * 60_000;
20
+ const SDK_REQUEST_TIMEOUT_MS = 30_000;
21
+ const DIRECT_COPILOT_COMPACTION_TIMEOUT_MS = 10 * 60_000;
22
+ const COPILOT_COMPACTION_RETRIES = 3;
23
+ const SDK_STOP_TIMEOUT_MS = 5_000;
24
+ const SESSION_LOCK_TIMEOUT_MS = 30_000;
25
+ const ACP_SESSION_OPEN_TIMEOUT_MS = 10 * 60_000;
26
+ const DIRECT_COPILOT_BACKGROUND_COMPACTION_THRESHOLD = COPILOT_COMPACTION_POLICY.background;
27
+ const DIRECT_COPILOT_BUFFER_EXHAUSTION_THRESHOLD = COPILOT_COMPACTION_POLICY.bufferExhaustion;
28
+ const COPILOT_BACKGROUND_COMPACTION_THRESHOLD = String(COPILOT_COMPACTION_POLICY.background);
29
+ const SUPPORTED_COMMANDS = ['compact', 'plan', 'goal'];
30
+ function compactionHasNothingToCompact(failure) {
31
+ const error = failure?.data?.error;
32
+ const message = typeof error === 'string' ? error : error?.message;
33
+ return typeof message === 'string' && /^\s*Nothing to compact\.?\s*$/i.test(message);
34
+ }
35
+ const sessionLocks = new Map();
36
+ const AGENCY_PLAN_INTERACTION_MARKER = 'AgentLink Plan interaction requirement:';
37
+ const LEGACY_NATIVE_AGENCY_PLAN_INTERACTION_GUIDANCE = [
38
+ AGENCY_PLAN_INTERACTION_MARKER,
39
+ 'Before writing or finalizing plan.md, if any user decision is needed, use ACP form elicitation so AgentLink receives a user_input_requested interaction.',
40
+ 'Ask one decision at a time, wait for the user answer, and continue this same Plan session only after the answer is returned.',
41
+ 'Do not put unresolved questions in plan.md, do not assume answers, and do not start implementation.',
42
+ 'Write the final plan.md only after all required decisions have been resolved.',
43
+ ].join(' ');
44
+ const ASK_USER_ONLY_AGENCY_PLAN_INTERACTION_GUIDANCE = [
45
+ AGENCY_PLAN_INTERACTION_MARKER,
46
+ 'Before writing or finalizing plan.md, if any user decision is needed, call the built-in ask_user tool; it is transported as ACP form elicitation so AgentLink receives a user_input_requested interaction.',
47
+ 'Ask exactly one decision at a time, wait for the user answer, and continue this same Plan session only after the answer is returned.',
48
+ 'Do not print unresolved questions in the response or put them in plan.md, do not assume answers, and do not start implementation.',
49
+ 'Write the final plan.md only after all required decisions have been resolved.',
50
+ ].join(' ');
51
+ const AGENCY_PLAN_ARTIFACT_INTERACTION_GUIDANCE = [
52
+ AGENCY_PLAN_INTERACTION_MARKER,
53
+ 'If the plan needs user decisions, record every unresolved question in plan.md under a dedicated confirmation section.',
54
+ 'Use the exact heading "## Decisions needed" for that section.',
55
+ 'Write each question as a numbered item and put any selectable choices in indented bullet items below that question.',
56
+ 'Do not assume answers or start implementation. AgentLink will collect the answers and return them in this Plan session.',
57
+ 'Answering one batch does not finish the Plan: reassess the requirements and add any newly required decisions in the next revision.',
58
+ ].join(' ');
59
+ const LEGACY_LONG_ARTIFACT_AGENCY_PLAN_INTERACTION_GUIDANCE = [
60
+ AGENCY_PLAN_INTERACTION_MARKER,
61
+ 'If the plan needs user decisions, record every unresolved question in plan.md under a dedicated confirmation section.',
62
+ 'Write each question as a numbered item and put any selectable choices in indented bullet items below that question.',
63
+ 'Do not assume answers or start implementation. AgentLink will collect the answers and return them in this Plan session.',
64
+ ].join(' ');
65
+ const LEGACY_SHORT_ARTIFACT_AGENCY_PLAN_INTERACTION_GUIDANCE = [
66
+ AGENCY_PLAN_INTERACTION_MARKER,
67
+ 'If the plan needs user decisions, record the unresolved questions in plan.md under a dedicated confirmation section.',
68
+ 'Do not assume answers or start implementation. AgentLink will collect the answers and return them in this Plan session.',
69
+ ].join(' ');
70
+ export function stripAgencyPlanInteractionGuidance(text) {
71
+ const trimmed = stripCopilotPlanControl(text).trimEnd();
72
+ const knownGuidance = [
73
+ LEGACY_NATIVE_AGENCY_PLAN_INTERACTION_GUIDANCE,
74
+ ASK_USER_ONLY_AGENCY_PLAN_INTERACTION_GUIDANCE,
75
+ AGENCY_PLAN_ARTIFACT_INTERACTION_GUIDANCE,
76
+ LEGACY_LONG_ARTIFACT_AGENCY_PLAN_INTERACTION_GUIDANCE,
77
+ LEGACY_SHORT_ARTIFACT_AGENCY_PLAN_INTERACTION_GUIDANCE,
78
+ ];
79
+ for (const guidance of knownGuidance) {
80
+ if (trimmed.endsWith(guidance)) {
81
+ return trimmed.slice(0, -guidance.length).trimEnd();
82
+ }
83
+ }
84
+ const markerIndex = trimmed.lastIndexOf(AGENCY_PLAN_INTERACTION_MARKER);
85
+ if (markerIndex >= 0) {
86
+ const possibleTruncatedGuidance = trimmed
87
+ .slice(markerIndex)
88
+ .replace(/(?:\.{3}|…)\s*$/, '')
89
+ .trimEnd();
90
+ if (knownGuidance.some(guidance => guidance.startsWith(possibleTruncatedGuidance))) {
91
+ return trimmed.slice(0, markerIndex).trimEnd();
92
+ }
93
+ }
94
+ return stripCopilotPlanControl(text);
95
+ }
96
+ export function copilotBootstrapCommands() {
97
+ return [
98
+ {
99
+ id: 'command:compact',
100
+ name: 'compact',
101
+ invocation: '/compact',
102
+ description: 'Summarize conversation history to reduce context window usage. Optionally provide focus instructions.',
103
+ kind: 'command',
104
+ },
105
+ {
106
+ id: 'command:plan',
107
+ name: 'plan',
108
+ invocation: '/plan',
109
+ description: 'Enter plan mode.',
110
+ kind: 'command',
111
+ },
112
+ {
113
+ id: 'command:goal',
114
+ name: 'goal',
115
+ invocation: '/goal',
116
+ description: 'Toggle autopilot mode, or set an autopilot objective with an optional AI-credit limit (--max-ai-credits)',
117
+ kind: 'command',
118
+ },
119
+ ];
120
+ }
121
+ function withTimeout(promise, timeoutMs, label) {
122
+ return new Promise((resolvePromise, rejectPromise) => {
123
+ const timer = setTimeout(() => rejectPromise(new Error(`${label} timed out`)), timeoutMs);
124
+ promise.then(value => {
125
+ clearTimeout(timer);
126
+ resolvePromise(value);
127
+ }, error => {
128
+ clearTimeout(timer);
129
+ rejectPromise(error);
130
+ });
131
+ });
132
+ }
133
+ function resolveDirectExecution(command) {
134
+ if (platform() !== 'win32' || !command.toLowerCase().endsWith('.cmd'))
135
+ return null;
136
+ const baseDir = dirname(command);
137
+ const loader = join(baseDir, 'node_modules', '@github', 'copilot', 'npm-loader.js');
138
+ if (existsSync(loader))
139
+ return { command: process.execPath, prefixArgs: [loader] };
140
+ try {
141
+ const match = readFileSync(command, 'utf8').match(/"%~dp0\\([^"\r\n]+\.js)"/);
142
+ if (match) {
143
+ const script = join(baseDir, match[1]);
144
+ if (existsSync(script))
145
+ return { command: process.execPath, prefixArgs: [script] };
146
+ }
147
+ }
148
+ catch {
149
+ // Fall through to cmd.exe below.
150
+ }
151
+ return null;
152
+ }
153
+ export function copilotMetadataScopeKey(scope) {
154
+ const spec = resolveCopilotCommandSpawn(scope.providerId, scope.workDir, {}, 'sdk');
155
+ const stamp = [spec.command, ...spec.args, resolveExecutablePath('copilot')].filter(Boolean).map(candidate => {
156
+ try {
157
+ const stat = statSync(candidate);
158
+ return `${candidate}:${stat.size}:${stat.mtimeMs}`;
159
+ }
160
+ catch {
161
+ return '';
162
+ }
163
+ });
164
+ return createHash('sha256').update(JSON.stringify([scope.providerId, spec.command, spec.args,
165
+ spec.options?.cwd, spec.options?.env, stamp])).digest('hex');
166
+ }
167
+ function validateCopilotSpawn(spec, providerId) {
168
+ const cwd = spec.options?.cwd;
169
+ if (typeof cwd !== 'string' || !cwd.trim())
170
+ throw new Error(`${providerId}: a working directory is required.`);
171
+ try {
172
+ if (!statSync(cwd).isDirectory())
173
+ throw new Error('Not a directory');
174
+ }
175
+ catch (error) {
176
+ const code = error.code || error.message;
177
+ throw new Error(`${providerId}: working directory is unavailable: ${cwd} (${code}). Please select an existing directory.`);
178
+ }
179
+ // Bare commands are resolved through PATH by spawn; do not guess another CLI
180
+ // or silently fall back to another working directory.
181
+ if (isAbsolute(spec.command) || /[\\/]/.test(spec.command)) {
182
+ const executable = isAbsolute(spec.command) ? spec.command : resolve(cwd, spec.command);
183
+ try {
184
+ if (!statSync(executable).isFile())
185
+ throw new Error('Not a file');
186
+ }
187
+ catch (error) {
188
+ const code = error.code || error.message;
189
+ throw new Error(`${providerId}: executable is unavailable: ${executable} (${code}). Check the backend installation/path.`);
190
+ }
191
+ }
192
+ }
193
+ function copilotSpawnError(spec, providerId, error) {
194
+ const cause = error;
195
+ return new Error(`${providerId}: runtime process failed${cause.code ? ` (${cause.code})` : ''}. `
196
+ + `Executable: ${spec.command}; working directory: ${String(spec.options?.cwd)}. ${cause.message || String(error)}`);
197
+ }
198
+ export function resolveCopilotCommandSpawn(providerId, workDir, sessionOptions = {}, transport = 'sdk') {
199
+ const provider = getProvider(providerId);
200
+ const isDefaultCopilot = providerId === 'copilot'
201
+ && provider.command === 'copilot'
202
+ && !provider.args?.length
203
+ && !provider.shell;
204
+ const resolved = isDefaultCopilot
205
+ ? { command: resolveExecutablePath('copilot') || 'copilot', prefixArgs: [], spawnOpts: {} }
206
+ : resolveProviderCommand(provider);
207
+ const runtimeArgs = [
208
+ ...resolved.prefixArgs,
209
+ ...(providerId === 'agency-copilot' && sessionOptions.contextTier
210
+ ? ['--context', sessionOptions.contextTier] : []),
211
+ ...(providerId === 'agency-copilot' && transport === 'acp'
212
+ ? ['--acp'] : [providerId === 'agency-copilot' ? '--server' : '--headless', '--no-auto-update', '--stdio']),
213
+ ];
214
+ const direct = resolveDirectExecution(resolved.command);
215
+ const runtimeEnv = {
216
+ ...getCleanEnv(),
217
+ ...(provider.env || {}),
218
+ ...((providerId === 'copilot' || providerId === 'agency-copilot')
219
+ ? { COPILOT_BACKGROUND_COMPACTION_THRESHOLD }
220
+ : {}),
221
+ };
222
+ if (direct) {
223
+ return {
224
+ command: direct.command,
225
+ args: [...direct.prefixArgs, ...runtimeArgs],
226
+ options: {
227
+ cwd: provider.cwd || workDir,
228
+ env: runtimeEnv,
229
+ stdio: ['pipe', 'pipe', 'pipe'],
230
+ windowsHide: true,
231
+ },
232
+ };
233
+ }
234
+ if (platform() === 'win32' && resolved.command.toLowerCase().endsWith('.cmd')) {
235
+ return {
236
+ command: process.env.COMSPEC || 'cmd.exe',
237
+ args: ['/d', '/s', '/c', resolved.command, ...runtimeArgs],
238
+ options: {
239
+ cwd: provider.cwd || workDir,
240
+ env: runtimeEnv,
241
+ stdio: ['pipe', 'pipe', 'pipe'],
242
+ windowsHide: true,
243
+ },
244
+ };
245
+ }
246
+ return {
247
+ command: resolved.command,
248
+ args: runtimeArgs,
249
+ options: {
250
+ cwd: provider.cwd || workDir,
251
+ env: runtimeEnv,
252
+ stdio: ['pipe', 'pipe', 'pipe'],
253
+ windowsHide: true,
254
+ ...resolved.spawnOpts,
255
+ },
256
+ };
257
+ }
258
+ export class CopilotCommandRuntime {
259
+ options;
260
+ cancelledCompactions = new Set();
261
+ compactionAttempts = new Map();
262
+ liveCompactions = new Map();
263
+ compactionLifecycle = new CopilotCompactionLifecycle();
264
+ goalSessions = new Set();
265
+ planRevisions = new Map();
266
+ preparePlanReview(sessionId) {
267
+ const revision = beginCopilotPlanState(this.options.sessionRoot || join(homedir(), '.copilot', 'session-state'), sessionId);
268
+ this.planRevisions.set(sessionId, revision);
269
+ return revision;
270
+ }
271
+ planControlPrompt(sessionId, revision, deferred = []) {
272
+ return copilotPlanControlPrompt(this.options.sessionRoot || join(homedir(), '.copilot', 'session-state'), sessionId, revision, deferred, !this.usesAgencyAcp() && !!this.options.onUserInputRequest);
273
+ }
274
+ readPlanReadiness(sessionId, revision) {
275
+ return readCopilotPlanState(this.options.sessionRoot || join(homedir(), '.copilot', 'session-state'), sessionId, revision);
276
+ }
277
+ sealPlanReadiness(sessionId, revision, content) {
278
+ sealCopilotPlanState(this.options.sessionRoot || join(homedir(), '.copilot', 'session-state'), sessionId, revision, content);
279
+ }
280
+ child = null;
281
+ connection = null;
282
+ acpConnection = null;
283
+ acpSessions = new Map();
284
+ acpCommandWaiters = new Map();
285
+ acpToolNames = new Map();
286
+ promptWaiters = new Map();
287
+ stderr = '';
288
+ releaseSessionLock = null;
289
+ cancelSessionLockWait = null;
290
+ stopPromise = null;
291
+ startPromise = null;
292
+ rejectStartup = null;
293
+ configuration = null;
294
+ configurationSince = 0;
295
+ configurationSessionId = '';
296
+ capabilityCheckedAt = 0;
297
+ capabilityDirty = false;
298
+ metadataScopeKey = '';
299
+ async configurationNeedsRefresh() {
300
+ if (!this.configuration || !this.options.verifyConfiguration)
301
+ return false;
302
+ if (this.options.strictCapabilities && !hasCopilotOfflineModel(this.options.providerId, this.configuration.requested.model))
303
+ return false;
304
+ if (this.capabilityDirty)
305
+ return true;
306
+ if (this.usesAgencyAcp() || Date.now() - this.capabilityCheckedAt < 300_000)
307
+ return false;
308
+ this.capabilityCheckedAt = Date.now();
309
+ try {
310
+ const candidate = createCopilotConfiguration(this.options.providerId, this.transport, this.configuration.requested);
311
+ applyModelCatalog(candidate, await this.request('models.list'), this.configuration);
312
+ this.capabilityDirty = !!candidate.capabilityFingerprint && candidate.capabilityFingerprint !== this.configuration.capabilityFingerprint;
313
+ return this.capabilityDirty;
314
+ }
315
+ catch (error) {
316
+ if (/model .* unavailable/i.test(String(error)))
317
+ throw error;
318
+ return false; // Existing confirmed configuration remains usable.
319
+ }
320
+ }
321
+ getConfiguration() {
322
+ return this.configuration ? structuredClone(this.configuration) : null;
323
+ }
324
+ beginConfiguration(sessionId, options) {
325
+ if (!this.options.verifyConfiguration)
326
+ return;
327
+ this.configurationSessionId = sessionId;
328
+ this.configurationSince = Date.now();
329
+ this.configuration = createCopilotConfiguration(this.options.providerId, this.transport, {
330
+ model: options.model, context: options.contextTier || 'default', effort: options.reasoningEffort,
331
+ });
332
+ this.configuration.sessionId = sessionId;
333
+ const previous = this.options.previousConfiguration;
334
+ if (previous?.effective.model === options.model)
335
+ this.configuration.observedDefaultWindow = previous?.observedDefaultWindow;
336
+ if (previous?.sessionId === sessionId && previous.providerId === this.options.providerId
337
+ && previous.windowEvidenceVersion === 1
338
+ && previous.effective.model === options.model
339
+ && previous.effective.context === (options.contextTier || 'default')
340
+ && ['provider_config', 'provider_event'].includes(previous.sources.contextWindow || '')) {
341
+ // Effort changes reasoning, not the selected model/context tier's input
342
+ // limit. Keep this observed window until fresh Provider evidence replaces it.
343
+ confirmConfiguration(this.configuration, 'contextWindow', previous.effective.contextWindow, previous.sources.contextWindow, previous.evidenceTimes?.contextWindow);
344
+ }
345
+ }
346
+ publishConfiguration() {
347
+ if (!this.configuration)
348
+ return;
349
+ this.configuration.runtime = { pid: this.child?.pid, phase: 'runtime' };
350
+ finishConfiguration(this.configuration);
351
+ this.options.onConfiguration?.(this.configurationSessionId, structuredClone(this.configuration));
352
+ }
353
+ observeConfiguration(sessionId, event) {
354
+ if (!this.configuration || sessionId !== this.configurationSessionId)
355
+ return;
356
+ const timestamp = typeof event.timestamp === 'string' ? Date.parse(event.timestamp) : NaN;
357
+ if (Number.isFinite(timestamp) && timestamp < this.configurationSince)
358
+ return;
359
+ const before = JSON.stringify(this.configuration.effective);
360
+ observeConfigurationEvent(this.configuration, event);
361
+ if (JSON.stringify(this.configuration.effective) !== before)
362
+ this.publishConfiguration();
363
+ }
364
+ async refreshConfiguration(sessionId) {
365
+ if (!this.configuration || sessionId !== this.configurationSessionId)
366
+ return;
367
+ if (!this.usesAgencyAcp()) {
368
+ try {
369
+ const current = await this.request('session.model.getCurrent', { sessionId }, 5000);
370
+ confirmConfiguration(this.configuration, 'model', current.modelId ?? current.model, 'provider_config');
371
+ confirmConfiguration(this.configuration, 'effort', current.reasoningEffort, 'provider_config');
372
+ confirmConfiguration(this.configuration, 'context', current.contextTier, 'provider_config');
373
+ confirmConfiguration(this.configuration, 'contextWindow', current.contextWindow, 'provider_config');
374
+ }
375
+ catch { /* Older Providers leave unreported fields explicitly unverified. */ }
376
+ }
377
+ readConfigurationEvents(this.configuration, this.options.sessionRoot || join(homedir(), '.copilot', 'session-state'), sessionId, this.configurationSince);
378
+ this.publishConfiguration();
379
+ if (this.configuration.status === 'failed')
380
+ throw new Error(this.configuration.issues.at(-1)?.message || 'Provider configuration failed');
381
+ }
382
+ /** Query an already-open Session only; never start a Runtime or send a prompt. */
383
+ async readContextSnapshot(sessionId) {
384
+ if (this.usesAgencyAcp())
385
+ return null;
386
+ const configuration = this.configuration;
387
+ const expectedModel = configuration?.effective.model;
388
+ const confirmedWindow = configuration?.effective.contextWindow;
389
+ if (!expectedModel || !confirmedWindow
390
+ || !['provider_config', 'provider_event'].includes(configuration?.sources.contextWindow || ''))
391
+ return null;
392
+ let response;
393
+ try {
394
+ // CLI 1.0.85 attribution reports the 128K default even for an observed
395
+ // 272K model. Never promote that diagnostic default (or a catalog/UI
396
+ // guess) to window evidence. Count against the already observed limit.
397
+ response = await this.request('session.metadata.contextInfo', {
398
+ sessionId, selectedModel: expectedModel, promptTokenLimit: confirmedWindow, outputTokenLimit: 0,
399
+ }, 5000);
400
+ }
401
+ catch (error) {
402
+ if (error?.code === -32601)
403
+ return null; // Older CLI.
404
+ throw error;
405
+ }
406
+ if (this.stopping || configuration !== this.configuration || configuration?.effective.model !== expectedModel
407
+ || configuration?.effective.contextWindow !== confirmedWindow)
408
+ throw new Error('Copilot context query was cancelled');
409
+ const value = response?.contextInfo;
410
+ if (!value)
411
+ return null; // A brand-new Session has no initialized context yet.
412
+ if (typeof value.modelName !== 'string' || value.modelName.toLowerCase() !== expectedModel.toLowerCase()) {
413
+ throw new Error('Copilot returned context usage for a different model');
414
+ }
415
+ const validTokens = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
416
+ if (!validTokens(value.totalTokens) || value.promptTokenLimit !== confirmedWindow) {
417
+ throw new Error('Copilot returned invalid context usage');
418
+ }
419
+ const snapshot = {
420
+ model: expectedModel,
421
+ contextTokens: value.totalTokens,
422
+ contextWindow: value.promptTokenLimit,
423
+ // Both SDK paths expose tools. Cold CLI 1.0.85 reports zero tool
424
+ // definitions until they are restored; a valid total alone is not proof
425
+ // of full context. MCP/custom-instruction counts are subsets, not extras.
426
+ contextComplete: validTokens(value.systemTokens) && value.systemTokens > 0
427
+ && validTokens(value.conversationTokens)
428
+ && validTokens(value.toolDefinitionsTokens) && value.toolDefinitionsTokens > 0
429
+ && value.systemTokens + value.conversationTokens + value.toolDefinitionsTokens === value.totalTokens,
430
+ ...(validTokens(value.compactionThreshold) ? { backgroundThreshold: value.compactionThreshold } : {}),
431
+ ...(validTokens(value.limit) && validTokens(value.bufferTokens) && value.limit >= value.bufferTokens
432
+ ? { blockingThreshold: value.limit - value.bufferTokens } : {}),
433
+ };
434
+ // The supplied limit is not new window evidence; do not publish it as such.
435
+ return snapshot;
436
+ }
437
+ stopping = false;
438
+ constructor(options) {
439
+ this.options = options;
440
+ }
441
+ start() {
442
+ if (this.startPromise)
443
+ return this.startPromise;
444
+ const starting = this.startRuntime().finally(() => {
445
+ if (this.startPromise === starting)
446
+ this.startPromise = null;
447
+ });
448
+ this.startPromise = starting;
449
+ return starting;
450
+ }
451
+ async startRuntime() {
452
+ if (this.stopPromise)
453
+ await this.stopPromise;
454
+ if (this.connection || this.acpConnection)
455
+ return;
456
+ const spec = resolveCopilotCommandSpawn(this.options.providerId, this.options.workDir, this.options.sessionOptions, this.transport);
457
+ validateCopilotSpawn(spec, this.options.providerId);
458
+ if (this.options.verifyConfiguration) {
459
+ this.beginConfiguration(this.options.initialSessionId || 'pending', this.options.sessionOptions || {});
460
+ this.options.onConfiguration?.(this.configurationSessionId, structuredClone(this.configuration));
461
+ }
462
+ this.stopping = false;
463
+ this.stderr = '';
464
+ this.metadataScopeKey = copilotMetadataScopeKey(this.options);
465
+ let child;
466
+ try {
467
+ child = spawn(spec.command, spec.args, spec.options);
468
+ }
469
+ catch (error) {
470
+ throw copilotSpawnError(spec, this.options.providerId, error);
471
+ }
472
+ this.child = child;
473
+ let starting = true;
474
+ let failureHandled = false;
475
+ let rejectStartup;
476
+ const startupFailure = new Promise((_, reject) => { rejectStartup = reject; });
477
+ // A child may fail before a protocol handshake or even stdio setup exists.
478
+ void startupFailure.catch(() => { });
479
+ this.rejectStartup = rejectStartup;
480
+ const fail = (error, exited = false) => {
481
+ if (this.child !== child || failureHandled)
482
+ return;
483
+ failureHandled = true;
484
+ rejectStartup(error);
485
+ if (exited)
486
+ this.child = null;
487
+ this.connection?.dispose();
488
+ this.connection = null;
489
+ this.acpConnection?.close();
490
+ this.acpConnection = null;
491
+ this.acpSessions.clear();
492
+ this.acpCommandWaiters.clear();
493
+ this.acpToolNames.clear();
494
+ this.goalSessions.clear();
495
+ for (const [id, attempt] of this.compactionAttempts) {
496
+ attempt.cancelled = true;
497
+ this.cancelledCompactions.add(id);
498
+ }
499
+ this.compactionAttempts.clear();
500
+ this.failPromptWaiters(error);
501
+ this.cancelSessionLockWait?.(error);
502
+ if (exited)
503
+ this.releaseSessionLock?.();
504
+ // During startup the rejecting start() owns reporting/cleanup. Calling
505
+ // onRuntimeExit here would make its caller mistake failure for cancellation.
506
+ const notifyExit = !starting && !this.stopping;
507
+ if (!starting && !exited) {
508
+ // Retain caller ownership until disposal finishes. A concurrent retry
509
+ // then sees this Runtime and joins stop(), rather than racing its lock.
510
+ const notify = () => { if (notifyExit)
511
+ this.options.onRuntimeExit?.(error); };
512
+ void this.stop().then(notify, notify);
513
+ }
514
+ else if (notifyExit)
515
+ this.options.onRuntimeExit?.(error);
516
+ };
517
+ // Keep the listener for the child's entire lifetime, including after stop:
518
+ // a late error on an old child must not crash Agent or poison its successor.
519
+ child.on('error', error => fail(copilotSpawnError(spec, this.options.providerId, error)));
520
+ child.stderr?.on('data', chunk => {
521
+ if (this.stderr.length < 16_384)
522
+ this.stderr += String(chunk);
523
+ });
524
+ child.once('exit', code => {
525
+ fail(copilotSpawnError(spec, this.options.providerId, new Error(`Process exited${code == null ? '' : ` with code ${code}`}`)), true);
526
+ });
527
+ let resolveSpawn;
528
+ const spawned = new Promise(resolve => { resolveSpawn = resolve; child.once('spawn', resolveSpawn); });
529
+ try {
530
+ // Never write connect/initialize to a failed spawn's pipes (in addition
531
+ // to ENOENT, that can trigger vscode-jsonrpc's unhandled write rejection).
532
+ await withTimeout(Promise.race([spawned, startupFailure]), SDK_START_TIMEOUT_MS, 'Copilot process startup');
533
+ if (this.stopping || this.child !== child)
534
+ throw new Error('Copilot runtime startup cancelled');
535
+ if (this.configuration) {
536
+ this.configuration.runtime = { pid: child.pid, phase: 'runtime' };
537
+ this.options.onConfiguration?.(this.configurationSessionId, structuredClone(this.configuration));
538
+ }
539
+ await this.initializeConnection(child, startupFailure, error => {
540
+ if (!this.stopping)
541
+ fail(error);
542
+ });
543
+ if (this.stopping || this.child !== child)
544
+ throw new Error('Copilot runtime startup cancelled');
545
+ }
546
+ catch (error) {
547
+ const detail = this.stderr.trim().split(/\r?\n/).slice(-4).join('\n');
548
+ await this.stop();
549
+ throw new Error(`${error.message}${detail ? `\n${detail}` : ''}`);
550
+ }
551
+ finally {
552
+ starting = false;
553
+ child.removeListener('spawn', resolveSpawn);
554
+ if (this.rejectStartup === rejectStartup)
555
+ this.rejectStartup = null;
556
+ }
557
+ }
558
+ async initializeConnection(child, startupFailure, onFailure) {
559
+ if (!child.stdout || !child.stdin) {
560
+ throw new Error('Copilot SDK runtime did not expose stdio pipes');
561
+ }
562
+ if (this.usesAgencyAcp()) {
563
+ const client = acp.client({ name: 'agentlink' })
564
+ .onNotification(acp.methods.client.session.update, ({ params }) => {
565
+ this.handleAcpSessionUpdate(params);
566
+ })
567
+ .onRequest(acp.methods.client.session.requestPermission, ({ params }) => {
568
+ const selected = params.options.find(option => option.kind === 'allow_always')
569
+ || params.options.find(option => option.kind === 'allow_once');
570
+ return selected
571
+ ? { outcome: { outcome: 'selected', optionId: selected.optionId } }
572
+ : { outcome: { outcome: 'cancelled' } };
573
+ })
574
+ .onRequest(acp.methods.client.elicitation.create, ({ params }) => {
575
+ if (!this.options.onAcpElicitationRequest) {
576
+ return { action: 'cancel' };
577
+ }
578
+ const requestId = `acp-elicitation:${randomUUID()}`;
579
+ const sessionId = 'sessionId' in params && typeof params.sessionId === 'string'
580
+ ? params.sessionId
581
+ : '';
582
+ if (sessionId)
583
+ this.pausePromptWaiter(sessionId, requestId);
584
+ return this.options.onAcpElicitationRequest(params).finally(() => {
585
+ if (sessionId)
586
+ this.resumePromptWaiter(sessionId, requestId);
587
+ });
588
+ });
589
+ const stream = acp.ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
590
+ const connection = client.connect(stream);
591
+ this.acpConnection = connection;
592
+ void connection.closed.then(() => onFailure(new Error('Agency Copilot ACP connection closed')), error => onFailure(error instanceof Error ? error : new Error(String(error))));
593
+ await withTimeout(Promise.race([
594
+ connection.agent.request(acp.methods.agent.initialize, {
595
+ protocolVersion: acp.PROTOCOL_VERSION,
596
+ clientCapabilities: {
597
+ session: { compaction: {}, configOptions: {} },
598
+ ...(this.options.onAcpElicitationRequest
599
+ ? { elicitation: { form: {} } }
600
+ : {}),
601
+ },
602
+ clientInfo: { name: 'agentlink', version: '1' },
603
+ }), startupFailure,
604
+ ]), AGENCY_ACP_START_TIMEOUT_MS, 'Agency Copilot ACP startup');
605
+ return;
606
+ }
607
+ const connection = createMessageConnection(new StreamMessageReader(child.stdout), new StreamMessageWriter(child.stdin));
608
+ this.connection = connection;
609
+ connection.onClose(() => onFailure(new Error('Copilot SDK connection closed')));
610
+ connection.onError(([error]) => onFailure(error));
611
+ connection.onNotification('session.event', notification => {
612
+ if (!notification || typeof notification !== 'object')
613
+ return;
614
+ const value = notification;
615
+ if (typeof value.sessionId === 'string'
616
+ && value.event
617
+ && typeof value.event === 'object') {
618
+ this.handleDirectSessionEvent(value.sessionId, value.event);
619
+ }
620
+ });
621
+ connection.onRequest('userInput.request', async (params) => {
622
+ if (!this.options.onUserInputRequest || !isRecord(params)) {
623
+ throw new Error('Copilot user input handler is unavailable');
624
+ }
625
+ const sessionId = typeof params.sessionId === 'string' ? params.sessionId : '';
626
+ const question = typeof params.question === 'string' ? params.question : '';
627
+ if (!sessionId || !question)
628
+ throw new Error('Invalid Copilot user input request');
629
+ const requestId = typeof params.requestId === 'string'
630
+ ? params.requestId
631
+ : `legacy:${question}`;
632
+ this.pausePromptWaiter(sessionId, requestId);
633
+ try {
634
+ return await this.options.onUserInputRequest(sessionId, {
635
+ question,
636
+ choices: Array.isArray(params.choices)
637
+ ? params.choices.filter((value) => typeof value === 'string')
638
+ : undefined,
639
+ allowFreeform: typeof params.allowFreeform === 'boolean'
640
+ ? params.allowFreeform
641
+ : undefined,
642
+ });
643
+ }
644
+ finally {
645
+ this.resumePromptWaiter(sessionId, requestId);
646
+ }
647
+ });
648
+ connection.onRequest('exitPlanMode.request', async (params) => {
649
+ if (!this.options.onExitPlanModeRequest || !isRecord(params)) {
650
+ throw new Error('Copilot Plan decision handler is unavailable');
651
+ }
652
+ const sessionId = typeof params.sessionId === 'string' ? params.sessionId : '';
653
+ const summary = typeof params.summary === 'string' ? params.summary : '';
654
+ const actions = Array.isArray(params.actions)
655
+ ? params.actions.filter((value) => typeof value === 'string')
656
+ : [];
657
+ const recommendedAction = typeof params.recommendedAction === 'string'
658
+ ? params.recommendedAction
659
+ : actions[0] || '';
660
+ if (!sessionId || !summary)
661
+ throw new Error('Invalid Copilot Plan decision request');
662
+ return this.options.onExitPlanModeRequest(sessionId, {
663
+ summary,
664
+ planContent: typeof params.planContent === 'string' ? params.planContent : undefined,
665
+ actions,
666
+ recommendedAction,
667
+ });
668
+ });
669
+ connection.listen();
670
+ await withTimeout(Promise.race([connection.sendRequest('connect', {}), startupFailure]), this.options.providerId === 'agency-copilot' ? AGENCY_ACP_START_TIMEOUT_MS : SDK_START_TIMEOUT_MS, 'Copilot SDK startup');
671
+ }
672
+ async request(method, params = {}, timeoutMs = SDK_REQUEST_TIMEOUT_MS) {
673
+ if (!this.connection)
674
+ throw new Error('Copilot SDK runtime is not started');
675
+ if (timeoutMs == null)
676
+ return this.connection.sendRequest(method, params);
677
+ return withTimeout(this.connection.sendRequest(method, params), timeoutMs, method);
678
+ }
679
+ async openSession(sessionId, options = {}) {
680
+ if (this.usesAgencyAcp()) {
681
+ return this.openAcpSession(sessionId, options);
682
+ }
683
+ const activeSessionId = sessionId || randomUUID();
684
+ await this.acquireSessionLock(activeSessionId);
685
+ this.beginConfiguration(activeSessionId, options);
686
+ const method = sessionId ? 'session.resume' : 'session.create';
687
+ let response;
688
+ try {
689
+ let effort = options.reasoningEffort;
690
+ if (this.configuration) {
691
+ let catalog;
692
+ try {
693
+ catalog = await this.request('models.list');
694
+ }
695
+ catch {
696
+ configurationIssue(this.configuration, 'effort', 'unconfirmed', 'Provider model capability catalog is unavailable.');
697
+ }
698
+ if (this.options.strictCapabilities) {
699
+ const status = await this.request('status.get').catch(() => undefined);
700
+ assertCopilotCapabilities(mergeCopilotCapabilities(this.options.providerId, catalog, status?.version), {
701
+ model: options.model, context: options.contextTier, effort: options.reasoningEffort,
702
+ });
703
+ }
704
+ const catalogModels = catalog?.models;
705
+ if (!this.options.strictCapabilities || (hasCopilotOfflineModel(this.options.providerId, options.model)
706
+ && (!Array.isArray(catalogModels) || catalogModels.some(m => m.id === options.model)))) {
707
+ effort = applyModelCatalog(this.configuration, catalog, this.options.previousConfiguration).effort;
708
+ }
709
+ this.capabilityCheckedAt = Date.now();
710
+ }
711
+ const params = {
712
+ sessionId: activeSessionId,
713
+ clientName: 'agentlink',
714
+ workingDirectory: this.options.workDir,
715
+ model: options.model,
716
+ contextTier: options.contextTier,
717
+ reasoningEffort: effort,
718
+ streaming: true,
719
+ includeSubAgentStreamingEvents: true,
720
+ requestUserInput: !!this.options.onUserInputRequest,
721
+ requestExitPlanMode: !!this.options.onExitPlanModeRequest,
722
+ toolFilterPrecedence: 'excluded',
723
+ envValueMode: 'direct',
724
+ // AgentLink handles the pre-request tier between turns; native tiers
725
+ // use the shared local policy. CLI acceptance alone is not
726
+ // proof it honors this policy; the publication barrier is separate.
727
+ infiniteSessions: {
728
+ enabled: true,
729
+ backgroundCompactionThreshold: DIRECT_COPILOT_BACKGROUND_COMPACTION_THRESHOLD,
730
+ bufferExhaustionThreshold: DIRECT_COPILOT_BUFFER_EXHAUSTION_THRESHOLD,
731
+ },
732
+ ...(sessionId ? { disableResume: true, continuePendingWork: false } : {}),
733
+ };
734
+ const timeout = this.options.providerId === 'agency-copilot' ? ACP_SESSION_OPEN_TIMEOUT_MS : SDK_REQUEST_TIMEOUT_MS;
735
+ for (let attempt = 0;; attempt++) {
736
+ try {
737
+ response = await this.request(method, params, timeout);
738
+ break;
739
+ }
740
+ catch (error) {
741
+ if (this.options.strictCapabilities)
742
+ throw error;
743
+ if (!this.configuration || attempt >= 2)
744
+ throw error;
745
+ const message = String(error);
746
+ if (params.contextTier === 'long_context' && /(?:context.*(?:unsupported|not supported|invalid)|unsupported.*context)/i.test(message)) {
747
+ configurationIssue(this.configuration, 'context', 'unsupported', message);
748
+ params.contextTier = 'default';
749
+ }
750
+ else if (params.reasoningEffort && /(?:effort.*(?:unsupported|not supported|invalid)|(?:unsupported|invalid).*effort)/i.test(message)) {
751
+ configurationIssue(this.configuration, 'effort', 'unsupported', message);
752
+ delete params.reasoningEffort;
753
+ }
754
+ else
755
+ throw error;
756
+ }
757
+ }
758
+ }
759
+ catch (error) {
760
+ if (this.configuration) {
761
+ this.configuration.status = 'failed';
762
+ configurationIssue(this.configuration, 'model', 'rejected', String(error));
763
+ this.publishConfiguration();
764
+ }
765
+ this.releaseSessionLock?.();
766
+ throw error;
767
+ }
768
+ const returnedSessionId = response.sessionId || activeSessionId;
769
+ if (returnedSessionId !== activeSessionId) {
770
+ this.releaseSessionLock?.();
771
+ throw new Error(`Copilot SDK returned unexpected session ID ${returnedSessionId}`);
772
+ }
773
+ await this.refreshConfiguration(returnedSessionId);
774
+ if (this.options.strictCapabilities && this.configuration) {
775
+ for (const field of ['model', 'context', 'effort']) {
776
+ if (field !== 'model' && !hasCopilotOfflineModel(this.options.providerId, options.model))
777
+ continue;
778
+ const requested = this.configuration.requested[field];
779
+ const actual = this.configuration.effective[field];
780
+ if (requested && actual && requested !== actual && this.configuration.sources[field] === 'provider_config') {
781
+ this.releaseSessionLock?.();
782
+ throw new Error(`Copilot did not apply the selected ${field}. Please choose another value in Options.`);
783
+ }
784
+ }
785
+ }
786
+ try {
787
+ await this.getMode(returnedSessionId);
788
+ }
789
+ catch { /* Older runtimes rely on subsequent mode events. */ }
790
+ return returnedSessionId;
791
+ }
792
+ async acquireSessionLock(sessionId) {
793
+ if (this.stopping)
794
+ throw new Error('Copilot runtime is stopped');
795
+ if (this.releaseSessionLock || this.cancelSessionLockWait) {
796
+ throw new Error('Copilot SDK runtime already owns a session lock');
797
+ }
798
+ const previous = sessionLocks.get(sessionId) || Promise.resolve();
799
+ let releaseCurrent;
800
+ const current = new Promise(resolvePromise => {
801
+ releaseCurrent = resolvePromise;
802
+ });
803
+ const tail = previous.then(() => current, () => current);
804
+ sessionLocks.set(sessionId, tail);
805
+ let cancelWait;
806
+ const cancelled = new Promise((_resolve, reject) => { cancelWait = reject; });
807
+ this.cancelSessionLockWait = cancelWait;
808
+ const timer = setTimeout(() => cancelWait(new Error(`Copilot Session ${sessionId} is still owned by another Runtime (lock wait timed out after 30 seconds). Stop the other request and retry.`)), SESSION_LOCK_TIMEOUT_MS);
809
+ try {
810
+ await Promise.race([previous.catch(() => { }), cancelled]);
811
+ if (this.stopping)
812
+ throw new Error('Copilot runtime stopped while waiting for its Session lock');
813
+ this.releaseSessionLock = () => {
814
+ releaseCurrent();
815
+ if (sessionLocks.get(sessionId) === tail)
816
+ sessionLocks.delete(sessionId);
817
+ this.releaseSessionLock = null;
818
+ };
819
+ }
820
+ catch (error) {
821
+ releaseCurrent();
822
+ // Keep the predecessor in the queue: a cancelled waiter must not let a
823
+ // later Runtime bypass an owner that still has the Session open.
824
+ void tail.then(() => {
825
+ if (sessionLocks.get(sessionId) === tail)
826
+ sessionLocks.delete(sessionId);
827
+ });
828
+ throw error;
829
+ }
830
+ finally {
831
+ clearTimeout(timer);
832
+ if (this.cancelSessionLockWait === cancelWait)
833
+ this.cancelSessionLockWait = null;
834
+ }
835
+ }
836
+ async listSessionCommands(sessionId) {
837
+ if (this.usesAgencyAcp()) {
838
+ const current = this.acpSessions.get(sessionId)?.commands;
839
+ if (current)
840
+ return current;
841
+ return withTimeout(new Promise(resolvePromise => {
842
+ const waiters = this.acpCommandWaiters.get(sessionId) || new Set();
843
+ waiters.add(resolvePromise);
844
+ this.acpCommandWaiters.set(sessionId, waiters);
845
+ }), SDK_REQUEST_TIMEOUT_MS, 'Agency Copilot command catalog');
846
+ }
847
+ return normalizeCopilotCommands(await this.request('session.commands.list', {
848
+ sessionId,
849
+ includeBuiltins: true,
850
+ includeSkills: true,
851
+ includeClientCommands: true,
852
+ }));
853
+ }
854
+ async invokeCommand(sessionId, name, input) {
855
+ this.cancelledCompactions.delete(sessionId);
856
+ if (name === 'compact' && !this.usesAgencyAcp()) {
857
+ return this.compactWithRetries(sessionId, input);
858
+ }
859
+ return this.invokeCommandOnce(sessionId, name, input);
860
+ }
861
+ /** Retry only a settled native failure, never a possibly still-running RPC. */
862
+ compactWithRetries(sessionId, input, initialFailure) {
863
+ const existing = this.compactionAttempts.get(sessionId);
864
+ if (existing?.promise)
865
+ return existing.promise;
866
+ const attempt = { cancelled: false };
867
+ this.compactionAttempts.set(sessionId, attempt);
868
+ let succeeded = false;
869
+ const checkActive = () => {
870
+ if (this.stopping)
871
+ throw new Error('Copilot runtime is stopped');
872
+ if (attempt.cancelled || this.compactionAttempts.get(sessionId) !== attempt) {
873
+ throw new Error('Copilot compaction cancelled');
874
+ }
875
+ };
876
+ // Defer one microtask so the operation owns the Session before any callback.
877
+ attempt.promise = Promise.resolve().then(async () => {
878
+ checkActive();
879
+ if (compactionHasNothingToCompact(initialFailure)) {
880
+ throw new Error('Context compaction failed: Nothing to compact.');
881
+ }
882
+ let lastFailure = initialFailure;
883
+ for (let retry = initialFailure ? 1 : 0; retry <= COPILOT_COMPACTION_RETRIES; retry++) {
884
+ checkActive();
885
+ if (retry)
886
+ console.info(`[Copilot] Retrying settled compaction: session=${sessionId} retry=${retry}/${COPILOT_COMPACTION_RETRIES}`);
887
+ attempt.failure = undefined;
888
+ attempt.completion = undefined;
889
+ let result;
890
+ let requestError;
891
+ try {
892
+ result = await this.invokeCommandOnce(sessionId, 'compact', input);
893
+ }
894
+ catch (error) {
895
+ requestError = error;
896
+ }
897
+ checkActive();
898
+ // Re-read callback-owned fields after await (they may have changed).
899
+ const { failure, completion } = this.compactionAttempts.get(sessionId);
900
+ if (failure) {
901
+ lastFailure = failure;
902
+ // A client deadline does not prove the native request has unwound,
903
+ // even if an earlier failed event was seen. Do not overlap attempts.
904
+ if (requestError && /timed out|disconnected|closed|stopped|cancelled/i.test(String(requestError)))
905
+ throw requestError;
906
+ if (compactionHasNothingToCompact(failure)) {
907
+ throw new Error('Context compaction failed: Nothing to compact.');
908
+ }
909
+ continue;
910
+ }
911
+ if (requestError)
912
+ throw requestError;
913
+ if (!result || result.kind === 'agent-prompt' || result.kind === 'select-subcommand') {
914
+ throw new Error('Provider did not complete context compaction; no chat message was resent.');
915
+ }
916
+ if (retry && completion?.data?.success !== true) {
917
+ throw new Error('Provider did not confirm that retried context compaction succeeded.');
918
+ }
919
+ // Publish the native completion only after its RPC has settled. This
920
+ // keeps the original output barrier and turn alive across all retries.
921
+ if (completion)
922
+ this.options.onSessionEvent?.(sessionId, completion);
923
+ succeeded = true;
924
+ return result;
925
+ }
926
+ const reason = lastFailure?.data?.error;
927
+ throw new Error(`Context compaction failed after ${COPILOT_COMPACTION_RETRIES} retries: ${typeof reason === 'string' ? reason : reason?.message || 'Provider reported unsuccessful compaction'}`);
928
+ }).finally(() => {
929
+ if (this.compactionAttempts.get(sessionId) !== attempt)
930
+ return;
931
+ this.compactionAttempts.delete(sessionId);
932
+ this.liveCompactions.get(sessionId)?.delete('main');
933
+ if (succeeded)
934
+ queueMicrotask(() => this.promptWaiters.get(sessionId)?.reconcile());
935
+ });
936
+ return attempt.promise;
937
+ }
938
+ async invokeCommandOnce(sessionId, name, input) {
939
+ const revision = name === 'plan' ? this.planRevisions.get(sessionId) : undefined;
940
+ const control = revision ? `\n\n${this.planControlPrompt(sessionId, revision)}` : '';
941
+ if (this.usesAgencyAcp()) {
942
+ const commandText = input.trim() ? `/${name} ${input}` : `/${name}`;
943
+ await this.waitForPrompt(sessionId, () => this.requestAcp(acp.methods.agent.session.prompt, {
944
+ sessionId,
945
+ prompt: [{
946
+ type: 'text',
947
+ text: name === 'plan'
948
+ ? `${commandText}\n\n${AGENCY_PLAN_ARTIFACT_INTERACTION_GUIDANCE}${control}`
949
+ : commandText,
950
+ }],
951
+ }, name === 'compact' ? DIRECT_COPILOT_COMPACTION_TIMEOUT_MS : null));
952
+ await this.refreshConfiguration(sessionId);
953
+ return { kind: 'completed' };
954
+ }
955
+ // Native maintenance must not replace the chat configuration with the
956
+ // compactor's/default model. Use an authoritative snapshot, never a guess
957
+ // from the selected UI options or the compaction token limit.
958
+ const beforeCompaction = name === 'compact' && this.options.verifyConfiguration
959
+ ? await this.request('session.model.getCurrent', { sessionId })
960
+ : undefined;
961
+ if (beforeCompaction) {
962
+ // A cold resume can invalidate the native model catalog after the earlier
963
+ // capability check. models.list is process/account metadata; only
964
+ // session.model.list populates this Session's native catalog using its
965
+ // own auth/integration. No probe chat or replacement Session is involved.
966
+ const catalog = await this.request('session.model.list', { sessionId });
967
+ const model = catalog.list?.find(item => item.id === beforeCompaction.modelId);
968
+ if (!model || model.policy?.state === 'disabled')
969
+ throw new Error('Cannot compact: the current model capability catalog is unavailable. Retry after the Provider is ready.');
970
+ if (this.stopping)
971
+ throw new Error('Copilot runtime is stopped');
972
+ }
973
+ try {
974
+ const result = await this.request('session.commands.invoke', { sessionId, name, input }, name === 'compact' ? DIRECT_COPILOT_COMPACTION_TIMEOUT_MS : SDK_REQUEST_TIMEOUT_MS);
975
+ if (beforeCompaction && !this.stopping) {
976
+ const after = await this.request('session.model.getCurrent', { sessionId });
977
+ const keys = ['modelId', 'contextTier', 'reasoningEffort'];
978
+ const changed = keys.some(key => typeof beforeCompaction[key] === 'string'
979
+ && beforeCompaction[key] !== after[key]);
980
+ if (changed) {
981
+ if (typeof beforeCompaction.modelId !== 'string' || !beforeCompaction.modelId) {
982
+ throw new Error('Context was compacted, but the original chat model could not be confirmed.');
983
+ }
984
+ await this.request('session.model.switchTo', {
985
+ sessionId, modelId: beforeCompaction.modelId,
986
+ ...(typeof beforeCompaction.contextTier === 'string' ? { contextTier: beforeCompaction.contextTier } : {}),
987
+ ...(typeof beforeCompaction.reasoningEffort === 'string' ? { reasoningEffort: beforeCompaction.reasoningEffort } : {}),
988
+ });
989
+ const restored = await this.request('session.model.getCurrent', { sessionId });
990
+ if (keys.some(key => typeof beforeCompaction[key] === 'string' && beforeCompaction[key] !== restored[key])) {
991
+ throw new Error('Context was compacted, but the Provider did not restore the original chat configuration.');
992
+ }
993
+ }
994
+ }
995
+ return control && result.kind === 'agent-prompt'
996
+ ? { ...result, prompt: `${result.prompt}${control}` }
997
+ : result;
998
+ }
999
+ finally {
1000
+ if (name === 'compact' && this.configuration)
1001
+ resetCopilotConfigurationObservation(this.configuration);
1002
+ }
1003
+ }
1004
+ async setMode(sessionId, mode) {
1005
+ if (this.usesAgencyAcp()) {
1006
+ const state = this.acpSessions.get(sessionId);
1007
+ const modeId = state?.modes?.availableModes.find(value => normalizeAcpMode(value.id) === mode)?.id
1008
+ || mode;
1009
+ await this.requestAcp(acp.methods.agent.session.setMode, { sessionId, modeId });
1010
+ if (state?.modes)
1011
+ state.modes.currentModeId = modeId;
1012
+ return;
1013
+ }
1014
+ await this.request('session.mode.set', { sessionId, mode });
1015
+ if (mode === 'autopilot')
1016
+ this.goalSessions.add(sessionId);
1017
+ else
1018
+ this.goalSessions.delete(sessionId);
1019
+ }
1020
+ async getMode(sessionId) {
1021
+ if (this.usesAgencyAcp()) {
1022
+ return normalizeAcpMode(this.acpSessions.get(sessionId)?.modes?.currentModeId);
1023
+ }
1024
+ const mode = await this.request('session.mode.get', { sessionId });
1025
+ if (mode === 'autopilot')
1026
+ this.goalSessions.add(sessionId);
1027
+ else if (mode === 'interactive' || mode === 'plan')
1028
+ this.goalSessions.delete(sessionId);
1029
+ return mode;
1030
+ }
1031
+ async enableAllowAll(sessionId) {
1032
+ if (this.usesAgencyAcp())
1033
+ return;
1034
+ await this.request('session.permissions.setMode', {
1035
+ sessionId,
1036
+ mode: 'allow-all',
1037
+ source: 'rpc',
1038
+ });
1039
+ }
1040
+ async send(sessionId, prompt, displayPrompt, attachments) {
1041
+ this.cancelledCompactions.delete(sessionId);
1042
+ if (this.usesAgencyAcp()) {
1043
+ await this.waitForPrompt(sessionId, () => this.requestAcp(acp.methods.agent.session.prompt, { sessionId, prompt: [{ type: 'text', text: prompt || displayPrompt }] }, null));
1044
+ // ACP does not expose context tier in configOptions on all versions.
1045
+ // Re-read this configuration generation's native journal after the real
1046
+ // prompt, when session/model evidence has been flushed by the Provider.
1047
+ await this.refreshConfiguration(sessionId);
1048
+ return;
1049
+ }
1050
+ await this.waitForPrompt(sessionId, () => this.request('session.send', {
1051
+ sessionId,
1052
+ prompt,
1053
+ displayPrompt,
1054
+ wait: true,
1055
+ ...(attachments?.length ? { attachments } : {}),
1056
+ }, null));
1057
+ await this.refreshConfiguration(sessionId);
1058
+ }
1059
+ readPlanContent(sessionId, sessionRoot = join(homedir(), '.copilot', 'session-state')) {
1060
+ const root = resolve(sessionRoot);
1061
+ const sessionDir = resolve(root, sessionId);
1062
+ if (sessionDir === root || !sessionDir.startsWith(`${root}${sep}`))
1063
+ return null;
1064
+ const planPath = join(sessionDir, 'plan.md');
1065
+ try {
1066
+ const content = readFileSync(planPath, 'utf8').trim();
1067
+ return content || null;
1068
+ }
1069
+ catch {
1070
+ return null;
1071
+ }
1072
+ }
1073
+ async abort(sessionId) {
1074
+ this.cancelledCompactions.add(sessionId);
1075
+ const attempt = this.compactionAttempts.get(sessionId);
1076
+ if (attempt)
1077
+ attempt.cancelled = true;
1078
+ this.compactionAttempts.delete(sessionId);
1079
+ this.liveCompactions.delete(sessionId);
1080
+ try {
1081
+ this.resumeAllPromptWaiters(sessionId);
1082
+ if (this.usesAgencyAcp()) {
1083
+ await this.notifyAcp(acp.methods.agent.session.cancel, { sessionId });
1084
+ }
1085
+ else {
1086
+ try {
1087
+ if (this.goalSessions.has(sessionId)) {
1088
+ await this.request('session.mode.set', { sessionId, mode: 'interactive' }, SDK_STOP_TIMEOUT_MS);
1089
+ this.goalSessions.delete(sessionId);
1090
+ }
1091
+ }
1092
+ finally {
1093
+ await this.request('session.abort', { sessionId }, SDK_STOP_TIMEOUT_MS);
1094
+ }
1095
+ }
1096
+ }
1097
+ finally {
1098
+ this.promptWaiters.get(sessionId)?.fail(new Error('Copilot request cancelled'));
1099
+ }
1100
+ }
1101
+ async respondToUserInput(sessionId, requestId, response) {
1102
+ if (this.usesAgencyAcp())
1103
+ return false;
1104
+ const result = await this.request('session.ui.handlePendingUserInput', {
1105
+ sessionId,
1106
+ requestId,
1107
+ response,
1108
+ });
1109
+ if (result.success === true)
1110
+ this.resumePromptWaiter(sessionId, requestId);
1111
+ return result.success === true;
1112
+ }
1113
+ async abortManualCompaction(sessionId) {
1114
+ this.cancelledCompactions.add(sessionId);
1115
+ const attempt = this.compactionAttempts.get(sessionId);
1116
+ if (attempt)
1117
+ attempt.cancelled = true;
1118
+ this.compactionAttempts.delete(sessionId);
1119
+ if (this.usesAgencyAcp()) {
1120
+ await this.abort(sessionId);
1121
+ return;
1122
+ }
1123
+ await this.request('session.history.abortManualCompaction', { sessionId });
1124
+ }
1125
+ async destroySession(sessionId) {
1126
+ if (this.usesAgencyAcp()) {
1127
+ try {
1128
+ await this.requestAcp(acp.methods.agent.session.close, { sessionId }, SDK_STOP_TIMEOUT_MS);
1129
+ }
1130
+ catch {
1131
+ // ACP connection shutdown below releases any remaining session resources.
1132
+ }
1133
+ this.acpSessions.delete(sessionId);
1134
+ return;
1135
+ }
1136
+ try {
1137
+ await this.request('session.destroy', { sessionId });
1138
+ }
1139
+ catch {
1140
+ // Runtime shutdown below also releases any remaining session handles.
1141
+ }
1142
+ }
1143
+ async deleteSession(sessionId) {
1144
+ if (this.options.providerId === 'agency-copilot')
1145
+ return;
1146
+ await this.request('session.delete', { sessionId });
1147
+ }
1148
+ stop() {
1149
+ if (this.stopPromise)
1150
+ return this.stopPromise;
1151
+ this.stopping = true;
1152
+ for (const attempt of this.compactionAttempts.values())
1153
+ attempt.cancelled = true;
1154
+ this.compactionAttempts.clear();
1155
+ this.liveCompactions.clear();
1156
+ this.compactionLifecycle = new CopilotCompactionLifecycle();
1157
+ this.rejectStartup?.(new Error('Copilot runtime startup cancelled'));
1158
+ this.cancelSessionLockWait?.(new Error('Copilot runtime stopped while waiting for its Session lock'));
1159
+ const stopped = this.stopRuntime().finally(() => {
1160
+ if (this.stopPromise === stopped)
1161
+ this.stopPromise = null;
1162
+ });
1163
+ this.stopPromise = stopped;
1164
+ return stopped;
1165
+ }
1166
+ async stopRuntime() {
1167
+ const connection = this.connection;
1168
+ const acpConnection = this.acpConnection;
1169
+ const child = this.child;
1170
+ let onClose;
1171
+ const closed = child?.pid && child.exitCode === null && child.signalCode === null && typeof child.once === 'function'
1172
+ ? new Promise(resolveClosed => { onClose = resolveClosed; child.once('close', onClose); })
1173
+ : Promise.resolve();
1174
+ this.connection = null;
1175
+ this.acpConnection = null;
1176
+ this.child = null;
1177
+ // Agency can still be fetching remote configuration before ACP initialize.
1178
+ // On Windows killing only agency.exe leaves its Git/credential helpers
1179
+ // alive. Terminate this owned tree before closing stdio (which can make
1180
+ // the parent exit before taskkill has enumerated its descendants).
1181
+ const forceAgencyTree = this.options.providerId === 'agency-copilot' && platform() === 'win32' && child?.pid
1182
+ && child.exitCode === null && child.signalCode === null;
1183
+ if (forceAgencyTree) {
1184
+ // Do not send runtime.shutdown into a pipe we are about to terminate.
1185
+ // vscode-jsonrpc's failed write can also escape as an unhandled rejection.
1186
+ connection?.dispose();
1187
+ await new Promise(resolve => {
1188
+ try {
1189
+ execFile('taskkill.exe', ['/pid', String(child.pid), '/t', '/f'], { timeout: SDK_STOP_TIMEOUT_MS, windowsHide: true }, () => resolve());
1190
+ }
1191
+ catch {
1192
+ resolve(); // The direct kill below is the bounded fallback.
1193
+ }
1194
+ });
1195
+ }
1196
+ if (acpConnection)
1197
+ acpConnection.close();
1198
+ if (connection && !forceAgencyTree) {
1199
+ try {
1200
+ await withTimeout(connection.sendRequest('runtime.shutdown', {}), SDK_STOP_TIMEOUT_MS, 'Copilot SDK shutdown');
1201
+ }
1202
+ catch {
1203
+ // Force termination below is the bounded fallback.
1204
+ }
1205
+ connection.dispose();
1206
+ }
1207
+ if (child && !child.killed)
1208
+ child.kill();
1209
+ // On Windows a successful kill/absent PID can precede close of stdio/cwd
1210
+ // handles. Do not let a retry outlive cleanup of its predecessor.
1211
+ try {
1212
+ await withTimeout(closed, SDK_STOP_TIMEOUT_MS, 'Copilot process shutdown');
1213
+ }
1214
+ finally {
1215
+ if (onClose)
1216
+ child?.removeListener('close', onClose);
1217
+ this.failPromptWaiters(new Error('Copilot runtime stopped'));
1218
+ }
1219
+ this.acpSessions.clear();
1220
+ this.goalSessions.clear();
1221
+ this.acpCommandWaiters.clear();
1222
+ this.acpToolNames.clear();
1223
+ this.releaseSessionLock?.();
1224
+ }
1225
+ isRunning() {
1226
+ return !!this.child && (!!this.connection || !!this.acpConnection);
1227
+ }
1228
+ get transport() {
1229
+ return this.options.providerId === 'agency-copilot' ? this.options.transport || 'sdk' : 'sdk';
1230
+ }
1231
+ usesAgencyAcp() {
1232
+ return this.transport === 'acp';
1233
+ }
1234
+ handleDirectSessionEvent(sessionId, event) {
1235
+ if (!this.observeCompactionLifecycle(sessionId, event))
1236
+ return;
1237
+ this.observeGoalLifecycle(sessionId, event);
1238
+ this.observeConfiguration(sessionId, event);
1239
+ const type = typeof event.type === 'string' ? event.type : '';
1240
+ const data = isRecord(event.data) ? event.data : undefined;
1241
+ const requestId = typeof data?.requestId === 'string' ? data.requestId : undefined;
1242
+ if (type === 'user_input.requested' && requestId) {
1243
+ this.pausePromptWaiter(sessionId, requestId);
1244
+ }
1245
+ else if (type === 'user_input.completed' && requestId) {
1246
+ this.resumePromptWaiter(sessionId, requestId);
1247
+ }
1248
+ this.options.onSessionEvent?.(sessionId, event);
1249
+ }
1250
+ observeGoalLifecycle(sessionId, event) {
1251
+ if (isCopilotChildEvent(event))
1252
+ return;
1253
+ if (event.type === 'session.mode_changed') {
1254
+ if (event.data?.newMode === 'autopilot')
1255
+ this.goalSessions.add(sessionId);
1256
+ else if (event.data?.newMode === 'interactive' || event.data?.newMode === 'plan')
1257
+ this.goalSessions.delete(sessionId);
1258
+ }
1259
+ if (event.type === 'session.task_complete')
1260
+ this.goalSessions.delete(sessionId);
1261
+ }
1262
+ observeCompactionLifecycle(sessionId, event) {
1263
+ if (!event.type?.startsWith('session.compaction_'))
1264
+ return true;
1265
+ if (this.stopping || this.cancelledCompactions.has(sessionId))
1266
+ return false;
1267
+ const scope = copilotChildScope(event) || 'main';
1268
+ const observed = this.compactionLifecycle.accept(sessionId, event);
1269
+ console.debug(`[CopilotCompactionBridge] ${JSON.stringify({ sessionId, scope, type: event.type,
1270
+ eventId: event.id, accepted: !!observed, late: observed?.late === true })}`);
1271
+ if (!observed)
1272
+ return false;
1273
+ event.agentlinkCompaction = observed.record;
1274
+ event.agentlinkCompactionLate = observed.late;
1275
+ if (observed.late)
1276
+ return true;
1277
+ const scopes = this.liveCompactions.get(sessionId) || new Set();
1278
+ const failed = event.type === 'session.compaction_failed' || event.data?.success === false;
1279
+ const attempt = scope === 'main' ? this.compactionAttempts.get(sessionId) : undefined;
1280
+ if (attempt && event.type !== 'session.compaction_start') {
1281
+ attempt.failure = failed ? event : undefined;
1282
+ attempt.completion = failed ? undefined : event;
1283
+ return false;
1284
+ }
1285
+ if (failed && scope === 'main' && !this.usesAgencyAcp() && !this.stopping
1286
+ && (scopes.has('main') || this.promptWaiters.has(sessionId))) {
1287
+ scopes.add('main');
1288
+ this.liveCompactions.set(sessionId, scopes);
1289
+ void this.compactWithRetries(sessionId, '', event).catch(error => {
1290
+ if (this.stopping || /compaction cancelled/.test(String(error)))
1291
+ return;
1292
+ this.options.onSessionEvent?.(sessionId, {
1293
+ type: 'session.compaction_failed',
1294
+ data: { error: error instanceof Error ? error.message : String(error) },
1295
+ });
1296
+ this.promptWaiters.get(sessionId)?.fail(error instanceof Error ? error : new Error(String(error)));
1297
+ void this.abort(sessionId).catch(() => { });
1298
+ });
1299
+ return false;
1300
+ }
1301
+ if (event.type === 'session.compaction_start')
1302
+ scopes.add(scope);
1303
+ else
1304
+ scopes.delete(scope);
1305
+ this.liveCompactions.set(sessionId, scopes);
1306
+ if (failed) {
1307
+ this.promptWaiters.get(sessionId)?.fail(new Error('Copilot native compaction failed'));
1308
+ void this.abort(sessionId).catch(() => { });
1309
+ }
1310
+ else if (!scopes.size) {
1311
+ queueMicrotask(() => this.promptWaiters.get(sessionId)?.reconcile());
1312
+ }
1313
+ return true;
1314
+ }
1315
+ waitForPrompt(sessionId, requestFactory) {
1316
+ if (this.promptWaiters.has(sessionId)) {
1317
+ return Promise.reject(new Error(`A Copilot prompt is already active for ${sessionId}`));
1318
+ }
1319
+ return new Promise((resolvePromise, rejectPromise) => {
1320
+ const waiter = {
1321
+ pendingUserInputIds: new Set(),
1322
+ settled: false,
1323
+ reconcile: () => { },
1324
+ reject: rejectPromise,
1325
+ fail: error => {
1326
+ if (waiter.settled)
1327
+ return;
1328
+ waiter.settled = true;
1329
+ this.clearPromptWaiter(sessionId, waiter);
1330
+ waiter.reject(error);
1331
+ },
1332
+ };
1333
+ this.promptWaiters.set(sessionId, waiter);
1334
+ let requestReturned = false;
1335
+ let requestValue;
1336
+ const journalPath = join(this.options.sessionRoot || join(homedir(), '.copilot', 'session-state'), sessionId, 'events.jsonl');
1337
+ if (this.usesAgencyAcp() && /^[\w-]+$/.test(sessionId) && existsSync(journalPath)) {
1338
+ const state = this.acpSessions.get(sessionId) || {};
1339
+ state.journalCompaction = true;
1340
+ this.acpSessions.set(sessionId, state);
1341
+ }
1342
+ const journal = /^[\w-]+$/.test(sessionId) ? new CopilotTurnJournal(journalPath, event => {
1343
+ this.observeGoalLifecycle(sessionId, event);
1344
+ const data = event.data || {};
1345
+ if (isCopilotChildEvent(event))
1346
+ return;
1347
+ if (event.type === 'user_input.requested' && data.requestId)
1348
+ this.pausePromptWaiter(sessionId, data.requestId);
1349
+ if (event.type === 'user_input.completed' && data.requestId)
1350
+ this.resumePromptWaiter(sessionId, data.requestId);
1351
+ // A persisted Goal summary must reach the same live reply path even
1352
+ // if its SDK notification was lost after an early send response.
1353
+ // The conversation handler already deduplicates task_complete text.
1354
+ if (event.type === 'session.task_complete') {
1355
+ this.options.onSessionEvent?.(sessionId, event);
1356
+ }
1357
+ // SDK notifications can be lost across a long background compaction.
1358
+ // Replay only its native lifecycle evidence; the driver deduplicates
1359
+ // event IDs/timestamps against the live stream, for both SDK backends.
1360
+ if (!this.usesAgencyAcp() && event.type?.startsWith('session.compaction_')) {
1361
+ this.handleDirectSessionEvent(sessionId, event);
1362
+ }
1363
+ if (!this.usesAgencyAcp())
1364
+ return;
1365
+ if (event.type === 'session.usage_checkpoint' || event.type?.startsWith('session.compaction_')) {
1366
+ const state = this.acpSessions.get(sessionId) || {};
1367
+ state.journalUsage = true;
1368
+ state.journalCompaction = true;
1369
+ this.acpSessions.set(sessionId, state);
1370
+ this.emitAcpEvent(sessionId, { ...event, journal: true });
1371
+ }
1372
+ else if (event.type === 'assistant.message' && data.phase === 'final_answer' && data.content) {
1373
+ this.emitAcpEvent(sessionId, { type: 'agentlink.journal_final', id: event.id, timestamp: event.timestamp, data });
1374
+ }
1375
+ }) : undefined;
1376
+ const settle = () => {
1377
+ if (waiter.settled || waiter.pendingUserInputIds.size > 0 || this.goalSessions.has(sessionId)
1378
+ || this.liveCompactions.get(sessionId)?.size || this.compactionAttempts.has(sessionId))
1379
+ return;
1380
+ // A background admission ACK is not completion. Conversely, a persisted
1381
+ // main final answer/Goal success + turn end + checkpoint can repair a
1382
+ // stuck RPC without mistaking a child completion for the whole turn.
1383
+ if ((requestReturned && !journal?.pending) || journal?.complete) {
1384
+ waiter.settled = true;
1385
+ this.clearPromptWaiter(sessionId, waiter);
1386
+ resolvePromise(requestValue);
1387
+ }
1388
+ };
1389
+ if (journal) {
1390
+ waiter.journalTimer = setInterval(() => { journal.read(); settle(); }, 1000);
1391
+ waiter.journalTimer.unref?.();
1392
+ }
1393
+ waiter.reconcile = () => { journal?.read(); settle(); };
1394
+ let request;
1395
+ try {
1396
+ request = requestFactory();
1397
+ }
1398
+ catch (error) {
1399
+ waiter.fail(error instanceof Error ? error : new Error(String(error)));
1400
+ return;
1401
+ }
1402
+ request.then(value => {
1403
+ if (waiter.settled)
1404
+ return;
1405
+ requestValue = value;
1406
+ requestReturned = true;
1407
+ journal?.read();
1408
+ settle();
1409
+ }, error => {
1410
+ if (waiter.settled)
1411
+ return;
1412
+ const compaction = this.compactionAttempts.get(sessionId)?.promise;
1413
+ if (compaction) {
1414
+ // A failed send is not replayable. Finish maintenance first, then
1415
+ // report the original failure instead of resending the user turn.
1416
+ void compaction.then(() => waiter.fail(error), failure => waiter.fail(failure));
1417
+ return;
1418
+ }
1419
+ waiter.settled = true;
1420
+ this.clearPromptWaiter(sessionId, waiter);
1421
+ rejectPromise(error);
1422
+ });
1423
+ });
1424
+ }
1425
+ pausePromptWaiter(sessionId, requestId) {
1426
+ const waiter = this.promptWaiters.get(sessionId);
1427
+ if (!waiter || waiter.settled)
1428
+ return;
1429
+ waiter.pendingUserInputIds.add(requestId);
1430
+ }
1431
+ resumePromptWaiter(sessionId, requestId) {
1432
+ const waiter = this.promptWaiters.get(sessionId);
1433
+ if (!waiter || waiter.settled)
1434
+ return;
1435
+ waiter.pendingUserInputIds.delete(requestId);
1436
+ queueMicrotask(() => { if (!waiter.settled)
1437
+ waiter.reconcile(); });
1438
+ }
1439
+ resumeAllPromptWaiters(sessionId) {
1440
+ const waiter = this.promptWaiters.get(sessionId);
1441
+ if (!waiter || waiter.settled)
1442
+ return;
1443
+ waiter.pendingUserInputIds.clear();
1444
+ }
1445
+ clearPromptWaiter(sessionId, waiter) {
1446
+ if (waiter.journalTimer)
1447
+ clearInterval(waiter.journalTimer);
1448
+ waiter.pendingUserInputIds.clear();
1449
+ if (this.promptWaiters.get(sessionId) === waiter) {
1450
+ this.promptWaiters.delete(sessionId);
1451
+ }
1452
+ }
1453
+ failPromptWaiters(error) {
1454
+ for (const waiter of [...this.promptWaiters.values()])
1455
+ waiter.fail(error);
1456
+ }
1457
+ async openAcpSession(sessionId, options = {}) {
1458
+ const lockId = sessionId || randomUUID();
1459
+ await this.acquireSessionLock(lockId);
1460
+ this.beginConfiguration(lockId, options);
1461
+ let activeSessionId;
1462
+ try {
1463
+ if (sessionId) {
1464
+ const state = this.acpSessions.get(sessionId) || {};
1465
+ state.suppressEvents = true;
1466
+ this.acpSessions.set(sessionId, state);
1467
+ }
1468
+ const response = sessionId
1469
+ ? await this.requestAcp(acp.methods.agent.session.load, { sessionId, cwd: this.options.workDir, mcpServers: [] }, ACP_SESSION_OPEN_TIMEOUT_MS)
1470
+ : await this.requestAcp(acp.methods.agent.session.new, { cwd: this.options.workDir, mcpServers: [] }, ACP_SESSION_OPEN_TIMEOUT_MS);
1471
+ activeSessionId = sessionId || response.sessionId;
1472
+ if (!activeSessionId)
1473
+ throw new Error('Agency Copilot ACP did not return a session ID');
1474
+ this.configurationSessionId = activeSessionId;
1475
+ if (this.configuration)
1476
+ this.configuration.sessionId = activeSessionId;
1477
+ const state = this.acpSessions.get(activeSessionId) || {};
1478
+ if (response && 'modes' in response)
1479
+ state.modes = response.modes;
1480
+ if (response && 'configOptions' in response)
1481
+ state.configOptions = response.configOptions;
1482
+ this.acpSessions.set(activeSessionId, state);
1483
+ await this.applyAcpSessionOptions(activeSessionId, state, options);
1484
+ state.suppressEvents = false;
1485
+ await this.refreshConfiguration(activeSessionId);
1486
+ return activeSessionId;
1487
+ }
1488
+ catch (error) {
1489
+ if (activeSessionId)
1490
+ this.acpSessions.delete(activeSessionId);
1491
+ else if (sessionId)
1492
+ this.acpSessions.delete(sessionId);
1493
+ if (this.configuration) {
1494
+ this.configuration.status = 'failed';
1495
+ configurationIssue(this.configuration, 'model', 'rejected', String(error));
1496
+ this.publishConfiguration();
1497
+ }
1498
+ this.releaseSessionLock?.();
1499
+ throw error;
1500
+ }
1501
+ }
1502
+ async applyAcpSessionOptions(sessionId, state, options) {
1503
+ const requested = [
1504
+ {
1505
+ label: 'model',
1506
+ value: options.model,
1507
+ categories: ['model'],
1508
+ namePattern: /model/i,
1509
+ },
1510
+ {
1511
+ label: 'context tier',
1512
+ value: options.contextTier,
1513
+ categories: ['model_config'],
1514
+ namePattern: /context/i,
1515
+ startupFallback: true,
1516
+ },
1517
+ {
1518
+ label: 'reasoning effort',
1519
+ value: options.reasoningEffort,
1520
+ categories: ['thought_level'],
1521
+ namePattern: /reason|thought|effort/i,
1522
+ },
1523
+ ];
1524
+ for (const target of requested) {
1525
+ if (!target.value)
1526
+ continue;
1527
+ const field = target.label === 'model' ? 'model' : target.label === 'context tier' ? 'context' : 'effort';
1528
+ const config = this.findAcpSelectConfig(state.configOptions, this.options.verifyConfiguration ? undefined : target.value, target.categories, target.namePattern);
1529
+ if (!config) {
1530
+ if (this.configuration)
1531
+ configurationIssue(this.configuration, field, 'unconfirmed', `Agency ACP does not report a ${target.label} configuration field.`);
1532
+ if (!target.startupFallback) {
1533
+ console.warn(`[Copilot] Agency ACP did not expose a compatible ${target.label} option for ${target.value}`);
1534
+ }
1535
+ continue;
1536
+ }
1537
+ const values = config.options.flatMap(entry => 'options' in entry ? entry.options : [entry]).map(entry => entry.value);
1538
+ if (this.configuration && field === 'effort')
1539
+ this.configuration.supportedEfforts = values;
1540
+ if (!values.includes(target.value)) {
1541
+ if (field === 'model')
1542
+ throw new Error(`Agency Copilot model ${target.value} is unavailable.`);
1543
+ const fallback = field === 'context' && values.includes('default') ? 'default'
1544
+ : values.includes(config.currentValue) ? config.currentValue : undefined;
1545
+ if (fallback && fallback !== config.currentValue) {
1546
+ const response = await this.requestAcp(acp.methods.agent.session.setConfigOption, { sessionId, configId: config.id, value: fallback });
1547
+ state.configOptions = response.configOptions;
1548
+ }
1549
+ if (this.configuration) {
1550
+ configurationIssue(this.configuration, field, 'unsupported', `Requested ${target.label} ${target.value} is unsupported.`);
1551
+ const actual = state.configOptions?.find(option => option.id === config.id);
1552
+ if (fallback && actual?.type === 'select')
1553
+ confirmConfiguration(this.configuration, field, actual.currentValue, 'provider_config');
1554
+ }
1555
+ continue;
1556
+ }
1557
+ if (config.currentValue !== target.value) {
1558
+ try {
1559
+ const response = await this.requestAcp(acp.methods.agent.session.setConfigOption, { sessionId, configId: config.id, value: target.value });
1560
+ state.configOptions = response.configOptions;
1561
+ }
1562
+ catch (error) {
1563
+ if (!this.configuration || field === 'model' || !/unsupported|not supported|invalid.*(?:effort|context)/i.test(String(error)))
1564
+ throw error;
1565
+ configurationIssue(this.configuration, field, 'unsupported', String(error));
1566
+ }
1567
+ }
1568
+ const actual = state.configOptions?.find(option => option.id === config.id);
1569
+ if (this.configuration && actual?.type === 'select') {
1570
+ confirmConfiguration(this.configuration, field, actual.currentValue, 'provider_config');
1571
+ }
1572
+ }
1573
+ }
1574
+ findAcpSelectConfig(options, requestedValue, categories, namePattern) {
1575
+ let best;
1576
+ let bestScore = 0;
1577
+ for (const option of options || []) {
1578
+ if (option.type !== 'select')
1579
+ continue;
1580
+ const values = option.options.flatMap(entry => ('options' in entry ? entry.options : [entry])).map(entry => entry.value);
1581
+ if (requestedValue !== undefined && !values.includes(requestedValue))
1582
+ continue;
1583
+ const identity = `${option.id} ${option.name}`;
1584
+ const score = (namePattern.test(identity) ? 2 : 0)
1585
+ + (option.category && categories.includes(option.category) ? 1 : 0);
1586
+ if (score > bestScore) {
1587
+ best = option;
1588
+ bestScore = score;
1589
+ }
1590
+ }
1591
+ return best;
1592
+ }
1593
+ async requestAcp(method, params, timeoutMs = SDK_REQUEST_TIMEOUT_MS) {
1594
+ if (!this.acpConnection)
1595
+ throw new Error('Agency Copilot ACP runtime is not started');
1596
+ if (timeoutMs == null) {
1597
+ return this.acpConnection.agent.request(method, params);
1598
+ }
1599
+ return withTimeout(this.acpConnection.agent.request(method, params), timeoutMs, method);
1600
+ }
1601
+ async notifyAcp(method, params) {
1602
+ if (!this.acpConnection)
1603
+ throw new Error('Agency Copilot ACP runtime is not started');
1604
+ await this.acpConnection.agent.notify(method, params);
1605
+ }
1606
+ handleAcpSessionUpdate(notification) {
1607
+ const { sessionId, update } = notification;
1608
+ const state = this.acpSessions.get(sessionId) || {};
1609
+ this.acpSessions.set(sessionId, state);
1610
+ switch (update.sessionUpdate) {
1611
+ case 'available_commands_update': {
1612
+ const commands = normalizeAcpCommands(update.availableCommands);
1613
+ state.commands = commands;
1614
+ const waiters = this.acpCommandWaiters.get(sessionId);
1615
+ if (waiters) {
1616
+ this.acpCommandWaiters.delete(sessionId);
1617
+ for (const resolvePromise of waiters)
1618
+ resolvePromise(commands);
1619
+ }
1620
+ break;
1621
+ }
1622
+ case 'current_mode_update':
1623
+ if (state.modes)
1624
+ state.modes.currentModeId = update.currentModeId;
1625
+ else
1626
+ state.modes = { currentModeId: update.currentModeId, availableModes: [] };
1627
+ this.emitAcpEvent(sessionId, {
1628
+ type: 'session.mode_changed',
1629
+ data: { newMode: normalizeAcpMode(update.currentModeId) },
1630
+ });
1631
+ break;
1632
+ case 'config_option_update':
1633
+ if (this.configuration && !state.suppressEvents) {
1634
+ const shape = (options) => JSON.stringify(options?.map(option => [option.id,
1635
+ option.type === 'select' ? option.options.flatMap(entry => 'options' in entry ? entry.options : [entry]).map(entry => entry.value).sort() : [],
1636
+ ]).sort((a, b) => String(a[0]).localeCompare(String(b[0]))));
1637
+ const previous = shape(state.configOptions);
1638
+ const next = shape(update.configOptions);
1639
+ if (previous !== next)
1640
+ this.capabilityDirty = true;
1641
+ }
1642
+ state.configOptions = update.configOptions;
1643
+ break;
1644
+ case 'compaction_update': {
1645
+ if (state.journalCompaction)
1646
+ break;
1647
+ const type = update.status === 'in_progress'
1648
+ ? 'session.compaction_start'
1649
+ : update.status === 'completed'
1650
+ ? 'session.compaction_complete'
1651
+ : 'session.compaction_failed';
1652
+ this.emitAcpEvent(sessionId, {
1653
+ type,
1654
+ data: { status: update.status, error: update.error },
1655
+ });
1656
+ break;
1657
+ }
1658
+ case 'usage_update':
1659
+ // Journal checkpoints own token counts, but ACP still supplies the
1660
+ // actual Session window. Do not discard that independent evidence.
1661
+ if (!state.suppressEvents && this.promptWaiters.has(sessionId)) {
1662
+ this.observeConfiguration(sessionId, { type: 'session.usage_update', data: { size: update.size } });
1663
+ }
1664
+ if (state.journalUsage)
1665
+ break;
1666
+ this.emitAcpEvent(sessionId, {
1667
+ type: 'session.usage_update',
1668
+ data: {
1669
+ used: update.used,
1670
+ size: update.size,
1671
+ cost: update.cost,
1672
+ },
1673
+ });
1674
+ break;
1675
+ case 'agent_message_chunk':
1676
+ case 'agent_thought_chunk':
1677
+ if (update.content.type === 'text') {
1678
+ this.emitAcpEvent(sessionId, {
1679
+ type: update.sessionUpdate === 'agent_message_chunk'
1680
+ ? 'assistant.message_delta'
1681
+ : 'assistant.reasoning_delta',
1682
+ data: { deltaContent: update.content.text },
1683
+ });
1684
+ }
1685
+ break;
1686
+ case 'tool_call': {
1687
+ const name = update.name || update.title;
1688
+ this.acpToolNames.set(update.toolCallId, name);
1689
+ this.emitAcpEvent(sessionId, {
1690
+ type: 'assistant.message',
1691
+ data: {
1692
+ toolRequests: [{
1693
+ toolCallId: update.toolCallId,
1694
+ name,
1695
+ arguments: update.rawInput,
1696
+ intentionSummary: update.title,
1697
+ }],
1698
+ },
1699
+ });
1700
+ break;
1701
+ }
1702
+ case 'tool_call_update':
1703
+ if (update.status === 'completed' || update.status === 'failed') {
1704
+ this.emitAcpEvent(sessionId, {
1705
+ type: 'tool.execution_complete',
1706
+ data: {
1707
+ toolCallId: update.toolCallId,
1708
+ toolName: update.name || this.acpToolNames.get(update.toolCallId),
1709
+ success: update.status === 'completed',
1710
+ result: { content: stringifyAcpToolOutput(update.rawOutput) },
1711
+ error: update.status === 'failed'
1712
+ ? { message: stringifyAcpToolOutput(update.rawOutput) || 'Tool execution failed' }
1713
+ : undefined,
1714
+ },
1715
+ });
1716
+ }
1717
+ break;
1718
+ }
1719
+ }
1720
+ emitAcpEvent(sessionId, event) {
1721
+ if (this.acpSessions.get(sessionId)?.suppressEvents)
1722
+ return;
1723
+ if (!this.observeCompactionLifecycle(sessionId, event))
1724
+ return;
1725
+ this.observeConfiguration(sessionId, event);
1726
+ this.options.onSessionEvent?.(sessionId, event);
1727
+ }
1728
+ }
1729
+ function normalizeAcpMode(modeId) {
1730
+ const normalized = modeId?.toLowerCase().split(/[#/]/).filter(Boolean).at(-1);
1731
+ if (normalized === 'plan')
1732
+ return 'plan';
1733
+ if (normalized === 'autopilot')
1734
+ return 'autopilot';
1735
+ return 'interactive';
1736
+ }
1737
+ function stringifyAcpToolOutput(value) {
1738
+ if (typeof value === 'string')
1739
+ return value;
1740
+ if (value == null)
1741
+ return '';
1742
+ try {
1743
+ return JSON.stringify(value);
1744
+ }
1745
+ catch {
1746
+ return String(value);
1747
+ }
1748
+ }
1749
+ export function normalizeAcpCommands(values) {
1750
+ if (!Array.isArray(values))
1751
+ return [];
1752
+ const normalized = [];
1753
+ for (const supported of SUPPORTED_COMMANDS) {
1754
+ const source = values.find(command => {
1755
+ const name = command?.name?.toLowerCase();
1756
+ return name === supported || (supported === 'goal' && name === 'autopilot');
1757
+ });
1758
+ if (!source)
1759
+ continue;
1760
+ normalized.push({
1761
+ id: `command:${supported}`,
1762
+ name: supported,
1763
+ invocation: `/${supported}`,
1764
+ description: source.description || '',
1765
+ kind: 'command',
1766
+ });
1767
+ }
1768
+ return normalized;
1769
+ }
1770
+ export function normalizeCopilotCommands(input) {
1771
+ const values = input?.commands;
1772
+ if (!Array.isArray(values))
1773
+ return [];
1774
+ const normalized = [];
1775
+ for (const supported of SUPPORTED_COMMANDS) {
1776
+ const source = values.find(command => {
1777
+ if (!command || typeof command.name !== 'string')
1778
+ return false;
1779
+ const names = [command.name, ...(Array.isArray(command.aliases) ? command.aliases : [])];
1780
+ return names.some(name => name.toLowerCase() === supported);
1781
+ });
1782
+ if (!source)
1783
+ continue;
1784
+ normalized.push({
1785
+ id: `command:${supported}`,
1786
+ name: supported,
1787
+ invocation: `/${supported}`,
1788
+ description: typeof source.description === 'string' ? source.description : '',
1789
+ kind: 'command',
1790
+ });
1791
+ }
1792
+ return normalized;
1793
+ }
1794
+ export async function discoverCopilotCommands(_scope) {
1795
+ return { commands: copilotBootstrapCommands() };
1796
+ }
1797
+ function isRecord(value) {
1798
+ return !!value && typeof value === 'object';
1799
+ }
1800
+ //# sourceMappingURL=copilot-command-bridge.js.map