@agent-link/agent 0.1.334 → 0.1.336

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/copilot.js CHANGED
@@ -10,18 +10,16 @@
10
10
  */
11
11
  import { spawn } from 'child_process';
12
12
  import { createInterface } from 'readline';
13
- import { readdirSync, readFileSync, writeFileSync, existsSync, rmSync, mkdirSync } from 'fs';
13
+ import { readdirSync, readFileSync, writeFileSync, existsSync, rmSync, mkdirSync, statSync } from 'fs';
14
14
  import { basename, dirname, extname, join, resolve } from 'path';
15
15
  import { homedir, platform } from 'os';
16
16
  import { randomUUID } from 'crypto';
17
- import * as acp from '@agentclientprotocol/sdk';
18
17
  import { processUserMessage } from './history-message-transform.js';
19
18
  import { copilotCapabilities } from './backends/types.js';
20
19
  import { CONFIG_DIR } from './config.js';
21
20
  import { getCleanEnv, resolveExecutablePath } from './sdk.js';
22
21
  import { getProvider, resolveProviderCommand } from './providers.js';
23
22
  import { FileCache } from './file-cache.js';
24
- import { CopilotCommandRuntime, } from './copilot-command-bridge.js';
25
23
  // ── Module state ───────────────────────────────────────────────────────────
26
24
  const conversations = new Map();
27
25
  const sendFns = new Set();
@@ -29,24 +27,6 @@ let globalModel = null;
29
27
  const MODEL_PATTERN = /^[a-zA-Z0-9._-]+$/;
30
28
  const CONTEXT_TIERS = new Set(['default', 'long_context']);
31
29
  const REASONING_EFFORTS = new Set(['none', 'low', 'medium', 'high', 'xhigh', 'max']);
32
- const COPILOT_PLAN_INPUT_TIMEOUT_MS = 10 * 60_000;
33
- const COPILOT_CONTINUE_PLAN_ANSWER = 'Continue developing the plan. In your final response, ask me to confirm the plan or describe any changes.';
34
- const pendingCopilotDecisions = new Map();
35
- class CopilotDecisionCancelledError extends Error {
36
- constructor() {
37
- super('Copilot decision request was cancelled');
38
- }
39
- }
40
- function setCopilotModeState(state, mode) {
41
- state.copilotMode = mode;
42
- state.planMode = mode === 'plan';
43
- sendFn({
44
- type: 'plan_mode_changed',
45
- enabled: state.planMode,
46
- mode,
47
- conversationId: state.conversationId,
48
- });
49
- }
50
30
  const COPILOT_NATIVE_ATTACHMENT_MIME_TYPES = new Set([
51
31
  'application/pdf',
52
32
  'image/gif',
@@ -80,107 +60,6 @@ export function addCopilotSendFn(fn) {
80
60
  sendFns.add(fn);
81
61
  return () => { sendFns.delete(fn); };
82
62
  }
83
- function emitCopilotDecisionRequest(pending) {
84
- sendFn({
85
- type: 'backend_event',
86
- conversationId: pending.conversationId,
87
- providerId: pending.providerId,
88
- claudeSessionId: pending.sessionId,
89
- event: {
90
- type: 'user_input_requested',
91
- session: sessionRef(pending.sessionId),
92
- requestId: pending.requestId,
93
- request: pending.request,
94
- },
95
- });
96
- }
97
- function settleCopilotDecision(pending, answer) {
98
- if (pendingCopilotDecisions.get(pending.requestId) !== pending)
99
- return false;
100
- pendingCopilotDecisions.delete(pending.requestId);
101
- if (pending.timer)
102
- clearTimeout(pending.timer);
103
- pending.timer = null;
104
- sendFn({
105
- type: 'backend_event',
106
- conversationId: pending.conversationId,
107
- providerId: pending.providerId,
108
- claudeSessionId: pending.sessionId,
109
- event: {
110
- type: 'user_input_resolved',
111
- session: sessionRef(pending.sessionId),
112
- requestId: pending.requestId,
113
- resolution: answer.action === 'cancel' ? 'cancelled' : 'answered',
114
- },
115
- });
116
- pending.resolve(answer);
117
- return true;
118
- }
119
- function waitForCopilotDecision(state, sessionId, request, choices = []) {
120
- const requestId = randomUUID();
121
- return new Promise(resolvePromise => {
122
- const pending = {
123
- requestId,
124
- conversationId: state.conversationId,
125
- providerId: state.providerId,
126
- sessionId,
127
- request,
128
- choices,
129
- resolve: resolvePromise,
130
- timer: null,
131
- };
132
- pendingCopilotDecisions.set(requestId, pending);
133
- if (request.expiresAt) {
134
- pending.timer = setTimeout(() => {
135
- if (choices.length > 0) {
136
- settleCopilotDecision(pending, { action: 'answer', answer: choices[0] });
137
- }
138
- else {
139
- settleCopilotDecision(pending, {
140
- action: 'answer',
141
- answer: COPILOT_CONTINUE_PLAN_ANSWER,
142
- });
143
- }
144
- }, Math.max(0, request.expiresAt - Date.now()));
145
- }
146
- emitCopilotDecisionRequest(pending);
147
- });
148
- }
149
- function cancelPendingCopilotDecisions(conversationId) {
150
- for (const pending of [...pendingCopilotDecisions.values()]) {
151
- if (pending.conversationId === conversationId) {
152
- settleCopilotDecision(pending, { action: 'cancel' });
153
- }
154
- }
155
- }
156
- export function getPendingCopilotDecisions(conversationId) {
157
- return [...pendingCopilotDecisions.values()]
158
- .filter(pending => pending.conversationId === conversationId)
159
- .map(pending => ({
160
- requestId: pending.requestId,
161
- conversationId: pending.conversationId,
162
- providerId: pending.providerId,
163
- sessionId: pending.sessionId,
164
- request: pending.request,
165
- }));
166
- }
167
- export function handleCopilotUserAnswer(requestId, answers, scope = {}) {
168
- const pending = pendingCopilotDecisions.get(requestId);
169
- if (!pending)
170
- return false;
171
- if (scope.conversationId && pending.conversationId !== scope.conversationId)
172
- return false;
173
- if (scope.providerId && pending.providerId !== scope.providerId)
174
- return false;
175
- const action = typeof answers.__action === 'string' ? answers.__action : undefined;
176
- const feedback = typeof answers.__feedback === 'string' ? answers.__feedback.trim() : undefined;
177
- const firstAnswer = Object.entries(answers)
178
- .find(([key, value]) => !key.startsWith('__') && typeof value === 'string')?.[1];
179
- const answer = action || (typeof firstAnswer === 'string' ? firstAnswer : '');
180
- if (!answer)
181
- return false;
182
- return settleCopilotDecision(pending, { action: 'answer', answer, feedback });
183
- }
184
63
  function resolveDirectExecution(cmdPath) {
185
64
  if (platform() !== 'win32')
186
65
  return null;
@@ -254,18 +133,10 @@ function finishCopilotTurn(state) {
254
133
  state.heartbeatTimer = null;
255
134
  }
256
135
  }
257
- function ensureCopilotCommandTurnActive(state, turnId) {
258
- if (!state.turnActive || state.turnId !== turnId) {
259
- throw new CopilotDecisionCancelledError();
260
- }
261
- }
262
136
  function emitCopilotCancelled(conversationId, state) {
263
137
  sendFn({
264
138
  type: 'backend_event',
265
139
  conversationId,
266
- claudeSessionId: state?.copilotSessionId || null,
267
- turnId: state?.turnId || null,
268
- stateVersion: state?.stateVersion || 0,
269
140
  event: { type: 'turn_cancelled', session: sessionRef(state?.copilotSessionId || '') },
270
141
  });
271
142
  sendFn({
@@ -334,10 +205,6 @@ export function rebindCopilotConversation(copilotSessionId, newConvId) {
334
205
  conversations.delete(oldConvId);
335
206
  state.conversationId = newConvId;
336
207
  conversations.set(newConvId, state);
337
- for (const pending of pendingCopilotDecisions.values()) {
338
- if (pending.conversationId === oldConvId)
339
- pending.conversationId = newConvId;
340
- }
341
208
  return true;
342
209
  }
343
210
  }
@@ -362,13 +229,6 @@ export function createCopilotPlaceholder(conversationId, opts) {
362
229
  stateVersion: 0,
363
230
  heartbeatTimer: null,
364
231
  sessionNotified: false,
365
- copilotMode: 'interactive',
366
- planMode: false,
367
- planModePending: false,
368
- isCompacting: false,
369
- commandRuntime: null,
370
- commandSessionId: null,
371
- deferredCommandUsage: null,
372
232
  });
373
233
  }
374
234
  export function restartCopilotConversation(conversationId, opts) {
@@ -383,7 +243,7 @@ export function restartCopilotConversation(conversationId, opts) {
383
243
  child: null,
384
244
  abortController: null,
385
245
  copilotSessionId: null,
386
- lastCopilotSessionId: opts?.preserveSession === false ? null : copilotSessionId,
246
+ lastCopilotSessionId: copilotSessionId,
387
247
  workDir: state?.workDir || process.cwd(),
388
248
  turnActive: false,
389
249
  providerId: opts?.providerId || state?.providerId || 'copilot',
@@ -394,13 +254,6 @@ export function restartCopilotConversation(conversationId, opts) {
394
254
  stateVersion: (state?.stateVersion || 0) + 1,
395
255
  heartbeatTimer: null,
396
256
  sessionNotified: false,
397
- copilotMode: state?.copilotMode || (state?.planMode ? 'plan' : 'interactive'),
398
- planMode: state?.planMode || false,
399
- planModePending: state?.planModePending || false,
400
- isCompacting: false,
401
- commandRuntime: null,
402
- commandSessionId: null,
403
- deferredCommandUsage: null,
404
257
  });
405
258
  return { wasTurnActive, copilotSessionId };
406
259
  }
@@ -414,459 +267,6 @@ export function setModel(conversationId, model) {
414
267
  globalModel = model;
415
268
  }
416
269
  }
417
- function detachCopilotProviderSession(state, providerId) {
418
- if (state.providerId === providerId)
419
- return;
420
- state.providerId = providerId;
421
- state.copilotSessionId = null;
422
- state.lastCopilotSessionId = null;
423
- state.sessionNotified = false;
424
- }
425
- function isMissingCopilotSessionError(error) {
426
- const message = error instanceof Error ? error.message : String(error);
427
- return /(?:session|resource).*not found|not found.*(?:session|resource)|no (?:session|task|name).*matched/i.test(message);
428
- }
429
- function emitCopilotCompactionStarted(state) {
430
- if (state.isCompacting)
431
- return false;
432
- state.isCompacting = true;
433
- const session = sessionRef(state.copilotSessionId || state.commandSessionId || 'pending');
434
- sendFn({
435
- type: 'context_compaction',
436
- conversationId: state.conversationId,
437
- turnId: state.turnId,
438
- status: 'started',
439
- });
440
- sendFn({
441
- type: 'backend_event',
442
- conversationId: state.conversationId,
443
- turnId: state.turnId,
444
- event: { type: 'compact_started', session },
445
- });
446
- return true;
447
- }
448
- function emitCopilotCompactionCompleted(state) {
449
- if (!state.isCompacting)
450
- return false;
451
- state.isCompacting = false;
452
- const session = sessionRef(state.copilotSessionId || state.commandSessionId || '');
453
- sendFn({
454
- type: 'context_compaction',
455
- conversationId: state.conversationId,
456
- turnId: state.turnId,
457
- status: 'completed',
458
- });
459
- sendFn({
460
- type: 'backend_event',
461
- conversationId: state.conversationId,
462
- turnId: state.turnId,
463
- event: { type: 'compact_completed', session },
464
- });
465
- return true;
466
- }
467
- function emitCopilotCommandOutput(state, content) {
468
- if (!content.trim())
469
- return;
470
- sendFn({
471
- type: 'command_output',
472
- conversationId: state.conversationId,
473
- content,
474
- });
475
- }
476
- function emitCopilotCommandCompleted(state) {
477
- if (!state.turnActive)
478
- return;
479
- finishCopilotTurn(state);
480
- const session = sessionRef(state.copilotSessionId || '');
481
- const usage = state.deferredCommandUsage || undefined;
482
- state.deferredCommandUsage = null;
483
- sendFn({
484
- type: 'backend_event',
485
- conversationId: state.conversationId,
486
- event: { type: 'turn_completed', session, ...(usage ? { usage } : {}) },
487
- });
488
- sendFn({
489
- type: 'turn_completed',
490
- conversationId: state.conversationId,
491
- workDir: state.workDir,
492
- claudeSessionId: state.copilotSessionId,
493
- turnId: state.turnId,
494
- stateVersion: state.stateVersion,
495
- ...(usage ? { usage } : {}),
496
- });
497
- }
498
- function commandInputForDisplay(command, input) {
499
- return input.trim() ? `/${command} ${input}` : `/${command}`;
500
- }
501
- async function applyCopilotCommandResult(state, runtime, sessionId, command, input, result) {
502
- switch (result.kind) {
503
- case 'text':
504
- emitCopilotCommandOutput(state, result.text);
505
- return;
506
- case 'completed':
507
- if (result.message)
508
- emitCopilotCommandOutput(state, result.message);
509
- return;
510
- case 'select-subcommand':
511
- throw new Error(`${result.title}: ${result.options.map(option => option.name).join(', ')}`);
512
- case 'agent-prompt':
513
- if (result.notice)
514
- emitCopilotCommandOutput(state, result.notice);
515
- if (result.mode) {
516
- await runtime.setMode(sessionId, result.mode);
517
- setCopilotModeState(state, result.mode);
518
- }
519
- await runtime.send(sessionId, result.prompt, result.displayPrompt || commandInputForDisplay(command, input));
520
- }
521
- }
522
- async function requestCopilotPlanInput(state, sessionId, request) {
523
- const choices = request.choices || [];
524
- const expiresAt = Date.now() + COPILOT_PLAN_INPUT_TIMEOUT_MS;
525
- const decision = await waitForCopilotDecision(state, sessionId, {
526
- questions: [{
527
- question: request.question,
528
- header: 'Plan question',
529
- options: choices.map(choice => ({ label: choice, value: choice })),
530
- }],
531
- source: 'tool_request_user_input',
532
- presentation: 'inline',
533
- kind: 'plan_input',
534
- allowFreeform: request.allowFreeform !== false,
535
- expiresAt,
536
- timeoutAction: choices.length > 0 ? 'first_option' : 'continue_plan',
537
- }, choices);
538
- if (decision.action === 'cancel')
539
- throw new CopilotDecisionCancelledError();
540
- const answer = decision.answer || '';
541
- return {
542
- answer,
543
- wasFreeform: !choices.includes(answer),
544
- };
545
- }
546
- async function requestCopilotAcpElicitation(state, request) {
547
- if (!acp.CreateElicitationRequest.isForm(request) || !('sessionId' in request)) {
548
- return { action: 'cancel' };
549
- }
550
- const properties = Object.entries(request.requestedSchema.properties || {});
551
- const [fieldName, field] = properties[0] || [];
552
- if (!fieldName || !field)
553
- return { action: 'decline' };
554
- let choices = [];
555
- let multiSelect = false;
556
- if (acp.ElicitationPropertySchema.isString(field)) {
557
- choices = field.oneOf?.map(option => option.const) || field.enum || [];
558
- }
559
- else if (acp.ElicitationPropertySchema.isArray(field)) {
560
- multiSelect = true;
561
- choices = acp.MultiSelectItems.isTitled(field.items)
562
- ? field.items.anyOf.map(option => option.const)
563
- : (acp.MultiSelectItems.isString(field.items) ? field.items.enum : []);
564
- }
565
- else if (acp.ElicitationPropertySchema.isBoolean(field)) {
566
- choices = ['true', 'false'];
567
- }
568
- const fieldRecord = field;
569
- const fieldDescription = typeof fieldRecord.description === 'string'
570
- ? fieldRecord.description
571
- : request.message;
572
- const fieldTitle = typeof fieldRecord.title === 'string'
573
- ? fieldRecord.title
574
- : 'Plan question';
575
- const expiresAt = Date.now() + COPILOT_PLAN_INPUT_TIMEOUT_MS;
576
- const decision = await waitForCopilotDecision(state, request.sessionId, {
577
- questions: [{
578
- question: fieldDescription,
579
- header: fieldTitle,
580
- options: choices.map(choice => ({ label: choice, value: choice })),
581
- multiSelect,
582
- }],
583
- source: 'tool_request_user_input',
584
- presentation: 'inline',
585
- kind: 'plan_input',
586
- allowFreeform: choices.length === 0,
587
- expiresAt,
588
- timeoutAction: choices.length > 0 ? 'first_option' : 'continue_plan',
589
- }, choices);
590
- if (decision.action === 'cancel')
591
- return { action: 'cancel' };
592
- const raw = decision.answer || '';
593
- let value = raw;
594
- if (acp.ElicitationPropertySchema.isArray(field))
595
- value = raw ? raw.split(/,\s*/) : [];
596
- else if (acp.ElicitationPropertySchema.isBoolean(field))
597
- value = raw === 'true';
598
- else if (acp.ElicitationPropertySchema.isNumber(field)
599
- || acp.ElicitationPropertySchema.isInteger(field))
600
- value = Number(raw);
601
- return { action: 'accept', content: { [fieldName]: value } };
602
- }
603
- function copilotPlanConfirmationPrompt(content) {
604
- return /[\u3400-\u9fff]/.test(content)
605
- ? '请确认以上计划,或直接回复需要修改的内容。'
606
- : 'Please confirm the Plan above, or reply with any changes you want.';
607
- }
608
- function emitCopilotPlanReview(state, sessionId, content, languageHint = content || '') {
609
- const plan = content?.trim() || '';
610
- const prompt = copilotPlanConfirmationPrompt(languageHint);
611
- sendFn({
612
- type: 'backend_event',
613
- conversationId: state.conversationId,
614
- providerId: state.providerId,
615
- claudeSessionId: sessionId,
616
- event: {
617
- type: 'text_delta',
618
- session: sessionRef(sessionId),
619
- text: `${plan ? `\n\n${plan}\n\n` : '\n\n'}> ${prompt}`,
620
- },
621
- });
622
- }
623
- async function acceptCopilotPlanExitWithoutBlocking(state, request) {
624
- const selectedAction = request.actions.includes('exit_only')
625
- ? 'exit_only'
626
- : request.actions.includes('interactive')
627
- ? 'interactive'
628
- : null;
629
- if (!selectedAction) {
630
- return {
631
- approved: false,
632
- feedback: COPILOT_CONTINUE_PLAN_ANSWER,
633
- };
634
- }
635
- state.planModePending = false;
636
- return { approved: true, selectedAction };
637
- }
638
- export async function executeCopilotCommand(conversationId, command, input, workDir, options) {
639
- let state = conversations.get(conversationId);
640
- if (!state) {
641
- createCopilotPlaceholder(conversationId, { providerId: options?.providerId });
642
- state = conversations.get(conversationId);
643
- }
644
- if (state.turnActive || state.commandRuntime) {
645
- sendFn({
646
- type: 'backend_event',
647
- conversationId,
648
- event: { type: 'error', message: 'Copilot is already processing this conversation' },
649
- });
650
- return;
651
- }
652
- state.workDir = workDir;
653
- const providerChanged = !!options?.providerId && state.providerId !== options.providerId;
654
- if (options?.providerId)
655
- detachCopilotProviderSession(state, options.providerId);
656
- const resumeSessionId = providerChanged
657
- ? undefined
658
- : options?.resumeSessionId
659
- || state.copilotSessionId
660
- || state.lastCopilotSessionId
661
- || undefined;
662
- const normalized = normalizeCopilotRunOptions(options?.copilotOptions);
663
- if (normalized.error) {
664
- sendCopilotSetupError(state, normalized.error);
665
- return;
666
- }
667
- if (normalized.options.model)
668
- state.model = normalized.options.model;
669
- if (normalized.options.context)
670
- state.contextTier = normalized.options.context;
671
- if (normalized.options.effort)
672
- state.reasoningEffort = normalized.options.effort;
673
- state.deferredCommandUsage = null;
674
- let pendingPlanReview = null;
675
- const runtime = new CopilotCommandRuntime({
676
- providerId: state.providerId,
677
- workDir,
678
- onSessionEvent: (eventSessionId, event) => {
679
- if (eventSessionId === state.commandSessionId)
680
- handleCopilotEvent(state, event);
681
- },
682
- onUserInputRequest: command === 'plan'
683
- ? (eventSessionId, request) => requestCopilotPlanInput(state, eventSessionId, request)
684
- : undefined,
685
- onExitPlanModeRequest: command === 'plan'
686
- ? (eventSessionId, request) => {
687
- pendingPlanReview = {
688
- sessionId: eventSessionId,
689
- content: request.planContent || request.summary,
690
- languageHint: `${request.summary}\n${request.planContent || ''}`,
691
- };
692
- return acceptCopilotPlanExitWithoutBlocking(state, request);
693
- }
694
- : undefined,
695
- onAcpElicitationRequest: command === 'plan'
696
- ? request => requestCopilotAcpElicitation(state, request)
697
- : undefined,
698
- });
699
- state.commandRuntime = runtime;
700
- const previousMode = state.copilotMode;
701
- startCopilotTurn(state);
702
- const commandTurnId = state.turnId;
703
- sendFn({
704
- type: 'backend_event',
705
- conversationId,
706
- event: {
707
- type: 'turn_started',
708
- session: sessionRef(resumeSessionId || 'pending'),
709
- },
710
- });
711
- if (command === 'compact')
712
- emitCopilotCompactionStarted(state);
713
- let sessionId = null;
714
- try {
715
- await runtime.start();
716
- ensureCopilotCommandTurnActive(state, commandTurnId);
717
- const sessionOptions = {
718
- model: state.model || globalModel || undefined,
719
- contextTier: state.contextTier || undefined,
720
- reasoningEffort: state.reasoningEffort || undefined,
721
- };
722
- try {
723
- sessionId = await runtime.openSession(resumeSessionId, sessionOptions);
724
- ensureCopilotCommandTurnActive(state, commandTurnId);
725
- }
726
- catch (error) {
727
- if (!resumeSessionId || !isMissingCopilotSessionError(error))
728
- throw error;
729
- state.copilotSessionId = null;
730
- state.lastCopilotSessionId = null;
731
- state.sessionNotified = false;
732
- sessionId = await runtime.openSession(undefined, sessionOptions);
733
- ensureCopilotCommandTurnActive(state, commandTurnId);
734
- }
735
- state.commandSessionId = sessionId;
736
- state.copilotSessionId = sessionId;
737
- state.lastCopilotSessionId = sessionId;
738
- await runtime.enableAllowAll(sessionId);
739
- ensureCopilotCommandTurnActive(state, commandTurnId);
740
- if (!state.sessionNotified) {
741
- state.sessionNotified = true;
742
- sendFn({
743
- type: 'backend_event',
744
- conversationId,
745
- event: { type: 'session_started', session: sessionRef(sessionId) },
746
- });
747
- }
748
- const commands = await runtime.listSessionCommands(sessionId);
749
- ensureCopilotCommandTurnActive(state, commandTurnId);
750
- if (!commands.some(value => value.name === command)) {
751
- throw new Error(`Copilot provider ${state.providerId} does not expose /${command}`);
752
- }
753
- if (command === 'plan')
754
- state.planModePending = true;
755
- if (command === 'goal')
756
- state.planModePending = false;
757
- const result = await runtime.invokeCommand(sessionId, command, input);
758
- ensureCopilotCommandTurnActive(state, commandTurnId);
759
- await applyCopilotCommandResult(state, runtime, sessionId, command, input, result);
760
- ensureCopilotCommandTurnActive(state, commandTurnId);
761
- if (command === 'compact')
762
- emitCopilotCompactionCompleted(state);
763
- const reportedMode = await runtime.getMode(sessionId);
764
- ensureCopilotCommandTurnActive(state, commandTurnId);
765
- const mode = command === 'plan'
766
- ? (state.planModePending ? 'plan' : reportedMode)
767
- : command === 'goal'
768
- ? 'autopilot'
769
- : reportedMode;
770
- state.planModePending = mode === 'plan';
771
- setCopilotModeState(state, mode);
772
- if (command === 'plan') {
773
- const review = pendingPlanReview || {
774
- sessionId,
775
- languageHint: input,
776
- };
777
- emitCopilotPlanReview(state, review.sessionId, review.content, review.languageHint);
778
- pendingPlanReview = null;
779
- }
780
- }
781
- catch (error) {
782
- pendingPlanReview = null;
783
- if (command === 'compact')
784
- state.isCompacting = false;
785
- if (!resumeSessionId && sessionId && state.copilotSessionId === sessionId) {
786
- state.copilotSessionId = null;
787
- state.lastCopilotSessionId = null;
788
- state.sessionNotified = false;
789
- }
790
- if (command === 'plan') {
791
- state.planModePending = true;
792
- setCopilotModeState(state, 'plan');
793
- }
794
- else if (command === 'goal') {
795
- setCopilotModeState(state, previousMode);
796
- }
797
- if (!(error instanceof CopilotDecisionCancelledError) && state.turnActive) {
798
- sendFn({
799
- type: 'backend_event',
800
- conversationId,
801
- event: {
802
- type: 'error',
803
- message: error instanceof Error ? error.message : String(error),
804
- },
805
- });
806
- }
807
- }
808
- finally {
809
- if (sessionId)
810
- await runtime.destroySession(sessionId);
811
- await runtime.stop();
812
- state.commandRuntime = null;
813
- state.commandSessionId = null;
814
- state.isCompacting = false;
815
- emitCopilotCommandCompleted(state);
816
- }
817
- }
818
- export async function setCopilotPlanMode(conversationId, enabled, workDir, providerId = 'copilot') {
819
- let state = conversations.get(conversationId);
820
- if (!state) {
821
- createCopilotPlaceholder(conversationId, { providerId });
822
- state = conversations.get(conversationId);
823
- }
824
- if (state.turnActive || state.commandRuntime) {
825
- throw new Error('Copilot is already processing this conversation');
826
- }
827
- state.workDir = workDir;
828
- detachCopilotProviderSession(state, providerId);
829
- if (enabled) {
830
- state.planModePending = true;
831
- setCopilotModeState(state, 'plan');
832
- return;
833
- }
834
- state.planModePending = false;
835
- const sessionId = state.copilotSessionId || state.lastCopilotSessionId;
836
- if (!sessionId) {
837
- setCopilotModeState(state, 'interactive');
838
- return;
839
- }
840
- const runtime = new CopilotCommandRuntime({ providerId, workDir });
841
- state.commandRuntime = runtime;
842
- state.commandSessionId = sessionId;
843
- let activeSessionId = sessionId;
844
- try {
845
- await runtime.start();
846
- try {
847
- await runtime.openSession(sessionId);
848
- }
849
- catch (error) {
850
- if (!isMissingCopilotSessionError(error))
851
- throw error;
852
- state.copilotSessionId = null;
853
- state.lastCopilotSessionId = null;
854
- state.sessionNotified = false;
855
- activeSessionId = await runtime.openSession();
856
- state.copilotSessionId = activeSessionId;
857
- state.lastCopilotSessionId = activeSessionId;
858
- state.commandSessionId = activeSessionId;
859
- }
860
- await runtime.setMode(activeSessionId, 'interactive');
861
- setCopilotModeState(state, await runtime.getMode(activeSessionId));
862
- }
863
- finally {
864
- await runtime.destroySession(activeSessionId);
865
- await runtime.stop();
866
- state.commandRuntime = null;
867
- state.commandSessionId = null;
868
- }
869
- }
870
270
  export function normalizeCopilotRunOptions(input) {
871
271
  const options = {};
872
272
  if (!input)
@@ -943,30 +343,26 @@ export function handleCopilotChat(conversationId, text, workDir, options, files)
943
343
  stateVersion: 0,
944
344
  heartbeatTimer: null,
945
345
  sessionNotified: false,
946
- copilotMode: 'interactive',
947
- planMode: false,
948
- planModePending: false,
949
- isCompacting: false,
950
- commandRuntime: null,
951
- commandSessionId: null,
952
- deferredCommandUsage: null,
953
346
  };
954
347
  conversations.set(conversationId, state);
955
348
  }
956
349
  state.workDir = workDir;
957
- const providerChanged = !!options?.providerId && state.providerId !== options.providerId;
958
- if (options?.providerId)
959
- detachCopilotProviderSession(state, options.providerId);
350
+ if (options?.providerId) {
351
+ state.providerId = options.providerId;
352
+ }
960
353
  // Determine session to resume
961
- const resumeId = providerChanged
962
- ? null
963
- : options?.resumeSessionId
964
- || state.copilotSessionId
965
- || state.lastCopilotSessionId;
354
+ const resumeId = options?.resumeSessionId
355
+ || state.copilotSessionId
356
+ || state.lastCopilotSessionId;
966
357
  if (resumeId) {
967
358
  state.copilotSessionId = resumeId;
968
359
  state.lastCopilotSessionId = resumeId;
969
360
  }
361
+ // For new sessions, pre-assign a sessionId via --session-id so we know it
362
+ // immediately (don't have to wait for the `result` event).
363
+ if (!resumeId) {
364
+ state.copilotSessionId = randomUUID();
365
+ }
970
366
  const normalized = normalizeCopilotRunOptions(options?.copilotOptions);
971
367
  if (normalized.error) {
972
368
  sendCopilotSetupError(state, normalized.error);
@@ -978,19 +374,6 @@ export function handleCopilotChat(conversationId, text, workDir, options, files)
978
374
  state.contextTier = normalized.options.context;
979
375
  if (normalized.options.effort)
980
376
  state.reasoningEffort = normalized.options.effort;
981
- if (state.planModePending) {
982
- void executeCopilotCommand(conversationId, 'plan', text, workDir, {
983
- resumeSessionId: resumeId || undefined,
984
- providerId: state.providerId,
985
- copilotOptions: options?.copilotOptions,
986
- });
987
- return;
988
- }
989
- // For new one-shot sessions, pre-assign a sessionId via --session-id so we
990
- // know it immediately (the SDK command path creates its own persisted ID).
991
- if (!resumeId) {
992
- state.copilotSessionId = randomUUID();
993
- }
994
377
  let processedFiles;
995
378
  try {
996
379
  processedFiles = processFilesForCopilot(files);
@@ -1111,20 +494,7 @@ export function handleCopilotChat(conversationId, text, workDir, options, files)
1111
494
  state.child = null;
1112
495
  state.abortController = null;
1113
496
  if (state.turnActive) {
1114
- const missingResumeSession = !!resumeId
1115
- && code !== 0
1116
- && isMissingCopilotSessionError(stderrBuf);
1117
497
  finishCopilotTurn(state);
1118
- if (missingResumeSession) {
1119
- state.copilotSessionId = null;
1120
- state.lastCopilotSessionId = null;
1121
- state.sessionNotified = false;
1122
- handleCopilotChat(conversationId, text, workDir, {
1123
- providerId: state.providerId,
1124
- copilotOptions: options?.copilotOptions,
1125
- }, files);
1126
- return;
1127
- }
1128
498
  // If turn wasn't completed by a result event, emit error or cancellation
1129
499
  if (code !== 0 && code !== null) {
1130
500
  const errMsg = stderrBuf.trim() || `Copilot process exited with code ${code}`;
@@ -1202,44 +572,6 @@ function handleCopilotEvent(state, event) {
1202
572
  }
1203
573
  const session = sessionRef(state.copilotSessionId || 'pending');
1204
574
  switch (event.type) {
1205
- case 'session.mode_changed': {
1206
- const data = event.data;
1207
- if (data?.newMode) {
1208
- const mode = data.newMode;
1209
- if (state.planModePending && mode === 'interactive')
1210
- break;
1211
- setCopilotModeState(state, mode);
1212
- }
1213
- break;
1214
- }
1215
- case 'session.compaction_start': {
1216
- emitCopilotCompactionStarted(state);
1217
- break;
1218
- }
1219
- case 'session.compaction_complete': {
1220
- // Native providers can report a compaction boundary before the command
1221
- // request itself settles. Keep the synthesized row pending until
1222
- // invokeCommand succeeds so a later rejection cannot leave a false
1223
- // completed checkmark behind.
1224
- if (state.commandRuntime && state.isCompacting)
1225
- break;
1226
- emitCopilotCompactionCompleted(state);
1227
- break;
1228
- }
1229
- case 'session.compaction_failed': {
1230
- state.isCompacting = false;
1231
- const data = event.data;
1232
- sendFn({
1233
- type: 'backend_event',
1234
- conversationId: convId,
1235
- turnId: state.turnId,
1236
- event: {
1237
- type: 'error',
1238
- message: data?.error || 'Context compaction failed',
1239
- },
1240
- });
1241
- break;
1242
- }
1243
575
  case 'assistant.message_delta': {
1244
576
  const data = event.data;
1245
577
  if (data?.deltaContent) {
@@ -1330,15 +662,8 @@ function handleCopilotEvent(state, event) {
1330
662
  state.copilotSessionId = sessionId;
1331
663
  state.lastCopilotSessionId = sessionId;
1332
664
  }
1333
- const usage = buildCopilotUsage(state, event.usage);
1334
- // Native command turns can remain open after Copilot finishes generating
1335
- // while AgentLink waits for a required final Plan decision. Their outer
1336
- // command lifecycle emits the authoritative terminal event.
1337
- if (state.commandRuntime) {
1338
- state.deferredCommandUsage = usage;
1339
- break;
1340
- }
1341
665
  finishCopilotTurn(state);
666
+ const usage = buildCopilotUsage(state, event.usage);
1342
667
  // Emit backend_event turn_completed
1343
668
  sendFn({
1344
669
  type: 'backend_event',
@@ -1366,23 +691,10 @@ function handleCopilotEvent(state, event) {
1366
691
  // ── Cancel ──────────────────────────────────────────────────────────────────
1367
692
  export function cancelCopilotExecution(conversationId) {
1368
693
  const state = conversations.get(conversationId);
1369
- cancelPendingCopilotDecisions(conversationId);
1370
694
  if (!state) {
1371
695
  emitCopilotCancelled(conversationId);
1372
696
  return;
1373
697
  }
1374
- state.deferredCommandUsage = null;
1375
- if (state.commandRuntime && state.commandSessionId) {
1376
- const cancellation = state.isCompacting
1377
- ? state.commandRuntime.abortManualCompaction(state.commandSessionId)
1378
- : state.commandRuntime.abort(state.commandSessionId);
1379
- state.isCompacting = false;
1380
- void cancellation.catch(() => { });
1381
- if (state.turnActive)
1382
- finishCopilotTurn(state);
1383
- emitCopilotCancelled(conversationId, state);
1384
- return;
1385
- }
1386
698
  if (!state.child) {
1387
699
  if (state.turnActive) {
1388
700
  finishCopilotTurn(state);
@@ -1411,7 +723,7 @@ function cleanupCopilotConversation(conversationId) {
1411
723
  const state = conversations.get(conversationId);
1412
724
  if (!state)
1413
725
  return;
1414
- if (state.child || state.commandRuntime) {
726
+ if (state.child) {
1415
727
  cancelCopilotExecution(conversationId);
1416
728
  }
1417
729
  if (state.heartbeatTimer) {
@@ -1553,8 +865,8 @@ const copilotWorkspaceCache = new FileCache({
1553
865
  ttlMs: 0,
1554
866
  debug: false,
1555
867
  });
1556
- function listCopilotWorkspaceFiles() {
1557
- if (!existsSync(COPILOT_SESSION_DIR))
868
+ function listCopilotWorkspaceFiles(strictReadErrors = false) {
869
+ if (!strictReadErrors && !existsSync(COPILOT_SESSION_DIR))
1558
870
  return [];
1559
871
  try {
1560
872
  const dirs = readdirSync(COPILOT_SESSION_DIR, { withFileTypes: true });
@@ -1563,24 +875,42 @@ function listCopilotWorkspaceFiles() {
1563
875
  if (!dir.isDirectory())
1564
876
  continue;
1565
877
  const wsPath = join(COPILOT_SESSION_DIR, dir.name, 'workspace.yaml');
1566
- if (existsSync(wsPath))
878
+ if (strictReadErrors) {
879
+ try {
880
+ statSync(wsPath);
881
+ files.push(wsPath);
882
+ }
883
+ catch (error) {
884
+ if (error.code !== 'ENOENT')
885
+ throw error;
886
+ }
887
+ }
888
+ else if (existsSync(wsPath)) {
1567
889
  files.push(wsPath);
890
+ }
1568
891
  }
1569
892
  return files;
1570
893
  }
1571
- catch {
894
+ catch (error) {
895
+ if (strictReadErrors && error.code !== 'ENOENT')
896
+ throw error;
1572
897
  return [];
1573
898
  }
1574
899
  }
1575
- function getCopilotSessionEntries(workDir) {
900
+ function getCopilotSessionEntries(workDir, strictReadErrors = false) {
1576
901
  const normalizedWorkDir = workDir ? normalizeCursorPath(workDir) : '';
1577
902
  const entries = [];
1578
- for (const wsPath of listCopilotWorkspaceFiles()) {
903
+ for (const wsPath of listCopilotWorkspaceFiles(strictReadErrors)) {
1579
904
  const parsed = copilotWorkspaceCache.get(wsPath);
905
+ if (!parsed && strictReadErrors)
906
+ throw new Error('Unable to read Copilot session workspace metadata');
1580
907
  if (!parsed)
1581
908
  continue;
1582
- if (normalizedWorkDir && normalizeCursorPath(parsed.projectPath) !== normalizedWorkDir)
909
+ if (workDir !== undefined && (!parsed.projectPath || normalizeCursorPath(parsed.projectPath) !== normalizedWorkDir))
1583
910
  continue;
911
+ if (strictReadErrors && parsed.sessionId !== basename(dirname(wsPath))) {
912
+ throw new Error('Copilot session ID does not match its workspace directory');
913
+ }
1584
914
  entries.push({
1585
915
  session: parsed,
1586
916
  sortKey: {
@@ -1593,11 +923,11 @@ function getCopilotSessionEntries(workDir) {
1593
923
  entries.sort((a, b) => compareCursorSortKeys(a.sortKey, b.sortKey));
1594
924
  return entries;
1595
925
  }
1596
- export function listCopilotSessions(workDir) {
1597
- return listCopilotSessionsPage(workDir, { limit: Number.POSITIVE_INFINITY }).sessions;
926
+ export function listCopilotSessions(workDir, options = {}) {
927
+ return listCopilotSessionsPage(workDir, { ...options, limit: Number.POSITIVE_INFINITY }).sessions;
1598
928
  }
1599
929
  export function listCopilotSessionsPage(workDir, options = {}) {
1600
- const entries = getCopilotSessionEntries(workDir);
930
+ const entries = getCopilotSessionEntries(workDir, options.strictReadErrors);
1601
931
  const limit = normalizeSessionLimit(options.limit);
1602
932
  let startIndex = 0;
1603
933
  if (options.cursor) {
@@ -1623,9 +953,9 @@ export function listCopilotSessionsPage(workDir, options = {}) {
1623
953
  };
1624
954
  }
1625
955
  // ── Session history ────────────────────────────────────────────────────────
1626
- export function readCopilotSessionMessages(_workDir, sessionId) {
956
+ export function readCopilotSessionMessages(_workDir, sessionId, options = {}) {
1627
957
  const eventsPath = join(COPILOT_SESSION_DIR, sessionId, 'events.jsonl');
1628
- if (!existsSync(eventsPath))
958
+ if (!options.strictReadErrors && !existsSync(eventsPath))
1629
959
  return [];
1630
960
  const messages = [];
1631
961
  try {
@@ -1717,42 +1047,13 @@ export function readCopilotSessionMessages(_workDir, sessionId) {
1717
1047
  }
1718
1048
  }
1719
1049
  }
1720
- catch {
1721
- // File not readable
1050
+ catch (error) {
1051
+ if (options.strictReadErrors)
1052
+ throw error;
1053
+ // Legacy history reads tolerate unreadable files.
1722
1054
  }
1723
1055
  return messages;
1724
1056
  }
1725
- export function readCopilotSessionMode(sessionId) {
1726
- const sessionRoot = resolve(COPILOT_SESSION_DIR);
1727
- const sessionDir = resolve(COPILOT_SESSION_DIR, sessionId);
1728
- if (sessionDir === sessionRoot || !sessionDir.startsWith(`${sessionRoot}${process.platform === 'win32' ? '\\' : '/'}`)) {
1729
- return 'interactive';
1730
- }
1731
- const eventsPath = join(sessionDir, 'events.jsonl');
1732
- if (!existsSync(eventsPath))
1733
- return 'interactive';
1734
- let mode = 'interactive';
1735
- try {
1736
- for (const line of readFileSync(eventsPath, 'utf-8').split('\n')) {
1737
- if (!line.trim())
1738
- continue;
1739
- try {
1740
- const event = JSON.parse(line);
1741
- const nextMode = event.type === 'session.mode_changed' ? event.data?.newMode : undefined;
1742
- if (nextMode === 'interactive' || nextMode === 'plan' || nextMode === 'autopilot') {
1743
- mode = nextMode;
1744
- }
1745
- }
1746
- catch {
1747
- // Ignore partial or malformed history lines.
1748
- }
1749
- }
1750
- }
1751
- catch {
1752
- return 'interactive';
1753
- }
1754
- return mode;
1755
- }
1756
1057
  export function getCopilotSessionWorkDir(sessionId) {
1757
1058
  const sessionRoot = resolve(COPILOT_SESSION_DIR);
1758
1059
  const sessionDir = resolve(COPILOT_SESSION_DIR, sessionId);