@agent-link/agent 0.1.338 → 0.1.340

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 (82) 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.d.ts +2 -1
  5. package/dist/connection.js +279 -48
  6. package/dist/connection.js.map +1 -1
  7. package/dist/copilot-capabilities-offline.d.ts +254 -0
  8. package/dist/copilot-capabilities-offline.js +392 -0
  9. package/dist/copilot-capabilities-offline.js.map +1 -0
  10. package/dist/copilot-capabilities.d.ts +43 -0
  11. package/dist/copilot-capabilities.js +128 -0
  12. package/dist/copilot-capabilities.js.map +1 -0
  13. package/dist/copilot-capability-service.d.ts +24 -0
  14. package/dist/copilot-capability-service.js +116 -0
  15. package/dist/copilot-capability-service.js.map +1 -0
  16. package/dist/copilot-command-bridge.d.ts +184 -0
  17. package/dist/copilot-command-bridge.js +1800 -0
  18. package/dist/copilot-command-bridge.js.map +1 -0
  19. package/dist/copilot-compaction-barrier.d.ts +36 -0
  20. package/dist/copilot-compaction-barrier.js +152 -0
  21. package/dist/copilot-compaction-barrier.js.map +1 -0
  22. package/dist/copilot-compaction-lifecycle.d.ts +34 -0
  23. package/dist/copilot-compaction-lifecycle.js +88 -0
  24. package/dist/copilot-compaction-lifecycle.js.map +1 -0
  25. package/dist/copilot-compaction-policy.d.ts +5 -0
  26. package/dist/copilot-compaction-policy.js +7 -0
  27. package/dist/copilot-compaction-policy.js.map +1 -0
  28. package/dist/copilot-configuration.d.ts +63 -0
  29. package/dist/copilot-configuration.js +283 -0
  30. package/dist/copilot-configuration.js.map +1 -0
  31. package/dist/copilot-event-scope.d.ts +13 -0
  32. package/dist/copilot-event-scope.js +26 -0
  33. package/dist/copilot-event-scope.js.map +1 -0
  34. package/dist/copilot-fork-metadata.d.ts +9 -0
  35. package/dist/copilot-fork-metadata.js +86 -0
  36. package/dist/copilot-fork-metadata.js.map +1 -0
  37. package/dist/copilot-message-kind.d.ts +21 -0
  38. package/dist/copilot-message-kind.js +45 -0
  39. package/dist/copilot-message-kind.js.map +1 -0
  40. package/dist/copilot-plan-state.d.ts +19 -0
  41. package/dist/copilot-plan-state.js +180 -0
  42. package/dist/copilot-plan-state.js.map +1 -0
  43. package/dist/copilot-request-dispatch.d.ts +10 -0
  44. package/dist/copilot-request-dispatch.js +31 -0
  45. package/dist/copilot-request-dispatch.js.map +1 -0
  46. package/dist/copilot-review-documents.d.ts +17 -0
  47. package/dist/copilot-review-documents.js +162 -0
  48. package/dist/copilot-review-documents.js.map +1 -0
  49. package/dist/copilot-session-activity.d.ts +4 -0
  50. package/dist/copilot-session-activity.js +185 -0
  51. package/dist/copilot-session-activity.js.map +1 -0
  52. package/dist/copilot-sidecar.d.ts +2 -0
  53. package/dist/copilot-sidecar.js +17 -0
  54. package/dist/copilot-sidecar.js.map +1 -0
  55. package/dist/copilot-turn-journal.d.ts +36 -0
  56. package/dist/copilot-turn-journal.js +246 -0
  57. package/dist/copilot-turn-journal.js.map +1 -0
  58. package/dist/copilot-user-requests.d.ts +11 -0
  59. package/dist/copilot-user-requests.js +84 -0
  60. package/dist/copilot-user-requests.js.map +1 -0
  61. package/dist/copilot.d.ts +202 -9
  62. package/dist/copilot.js +5120 -243
  63. package/dist/copilot.js.map +1 -1
  64. package/dist/file-cache.d.ts +6 -1
  65. package/dist/file-cache.js +62 -1
  66. package/dist/file-cache.js.map +1 -1
  67. package/dist/history-message-transform.d.ts +47 -1
  68. package/dist/history-message-transform.js +67 -0
  69. package/dist/history-message-transform.js.map +1 -1
  70. package/dist/history.d.ts +8 -0
  71. package/dist/history.js.map +1 -1
  72. package/dist/provider-command-discovery.d.ts +5 -0
  73. package/dist/provider-command-discovery.js +49 -1
  74. package/dist/provider-command-discovery.js.map +1 -1
  75. package/dist/provider-command-types.d.ts +5 -0
  76. package/dist/provider-command-types.js.map +1 -1
  77. package/dist/runtime.d.ts +1 -1
  78. package/dist/runtime.js +1 -1
  79. package/dist/runtime.js.map +1 -1
  80. package/dist/search-sessions.js +2 -1
  81. package/dist/search-sessions.js.map +1 -1
  82. package/package.json +3 -1
@@ -1,4 +1,7 @@
1
1
  import WebSocket from 'ws';
2
+ import { setCopilotProvider } from './runtime.js';
3
+ import { dispatchCopilotRequest, resolveCopilotCommandResumeSessionId } from './copilot-request-dispatch.js';
4
+ import { COPILOT_COMPACTION_POLICY } from './copilot-compaction-policy.js';
2
5
  import os from 'os';
3
6
  import path from 'path';
4
7
  import { performance } from 'perf_hooks';
@@ -29,12 +32,15 @@ const require = createRequire(import.meta.url);
29
32
  const pkg = require('../package.json');
30
33
  const TEAM_SESSIONS_DIR = path.join(CONFIG_DIR, 'team-sessions');
31
34
  const LOOP_SESSIONS_DIR = path.join(CONFIG_DIR, 'loop-sessions');
32
- import { handleChat as claudeHandleChat, addSendFn, abort as abortClaude, abortAll as abortAllClaude, cancelExecution as claudeCancelExecution, handleUserAnswer, handleToolPermissionResponse, handleBtwQuestion, getConversation, getConversations, getIsCompacting, clearSessionId, evictByClaudeSessionId, rebindConversation, addOutputObserver, removeOutputObserver, addCloseObserver, removeCloseObserver, setOutputObserver, clearOutputObserver, setCloseObserver, clearCloseObserver, restartConversation, createPlaceholderConversation, getPendingQuestions, getPendingToolPermissions, setModel, addOnSessionStarted, getBackendType, setBackendType, isCopilotBackend, isCodexBackend, isPiBackend, isYeaftBackend, addCopilotSendFn, handleCopilotChat, cancelCopilotExecution, listCopilotSessionsPage, listAllCopilotSessionsPage, readCopilotSessionMessages, getCopilotSessionWorkDir, deleteCopilotSession, renameCopilotSession, moveCopilotSession, getCopilotConversation, getCopilotConversations, createCopilotPlaceholder, restartCopilotConversation, rebindCopilotConversation, cleanupAllCopilotConversations, copilotCapabilities, addCodexSendFn, handleCodexChat, cancelCodexExecution, listCodexSessionsPage, listAllCodexSessionsPage, readCodexSessionMessages, deleteCodexSession, renameCodexSession, moveCodexSession, getCodexConversation, getCodexConversations, createCodexPlaceholder, restartCodexConversation, rebindCodexConversation, cleanupAllCodexConversations, evictByCodexSessionId, codexCapabilities } from './runtime.js';
35
+ import { handleChat as claudeHandleChat, addSendFn, abort as abortClaude, abortAll as abortAllClaude, cancelExecution as claudeCancelExecution, handleUserAnswer, handleToolPermissionResponse, handleBtwQuestion, getConversation, getConversations, getIsCompacting, clearSessionId, evictByClaudeSessionId, rebindConversation, addOutputObserver, removeOutputObserver, addCloseObserver, removeCloseObserver, setOutputObserver, clearOutputObserver, setCloseObserver, clearCloseObserver, restartConversation, createPlaceholderConversation, getPendingQuestions, getPendingToolPermissions, setModel, addOnSessionStarted, getBackendType, setBackendType, isCopilotBackend, isCodexBackend, isPiBackend, isYeaftBackend, addCopilotSendFn, handleCopilotChat, executeCopilotCommand, setCopilotPlanMode, startCopilotPlanImplementation, cancelCopilotExecution, readCopilotSessionMessages, readCopilotSessionMode, readCopilotSessionProvider, readCopilotSessionUsage, getCopilotSessionWorkDir, deleteCopilotSession, renameCopilotSession, moveCopilotSession, getCopilotConversation, getCopilotConversations, createCopilotPlaceholder, rebindCopilotConversation, getPendingCopilotDecisions, handleCopilotUserAnswer, cleanupAllCopilotConversations, copilotCapabilities, addCodexSendFn, handleCodexChat, cancelCodexExecution, listCodexSessionsPage, listAllCodexSessionsPage, readCodexSessionMessages, deleteCodexSession, renameCodexSession, moveCodexSession, getCodexConversation, getCodexConversations, createCodexPlaceholder, restartCodexConversation, rebindCodexConversation, cleanupAllCodexConversations, evictByCodexSessionId, codexCapabilities } from './runtime.js';
33
36
  import { addPiSendFn, cancelPiExecution, cleanupAllPiConversations, createPiPlaceholder, deletePiSession, evictByPiSessionId, getPiConversation, getPiConversations, getPiSessionWorkDir, handlePiChat, listAllPiSessionsPage, listPiSessionsPage, movePiSession, piCapabilities, readPiSessionMessages, rebindPiConversation, renamePiSession, restartPiConversation, } from './pi.js';
34
37
  import { addYeaftSendFn, cancelYeaftExecution, cleanupAllYeaftConversations, createYeaftPlaceholder, deleteYeaftSession, evictByYeaftSessionId, getYeaftConversation, getYeaftConversations, handleYeaftChat, handleYeaftUserAnswer, listAllYeaftSessionsPage, listYeaftSessionsPage, moveYeaftSession, readYeaftSessionMessages, rebindYeaftConversation, renameYeaftSession, restartYeaftConversation, } from './yeaft.js';
35
38
  import { DEFAULT_SESSION_LIST_LIMIT, LEGACY_SESSION_LIST_CAP, listSessions, listSessionsPageAsync, readSessionMessages, deleteSession, renameSession, moveSession, listAllRecentSessionsPage, getSessionCacheStats } from './history.js';
36
39
  import { discoverProviderCommands, providerCommandDiscoverers, } from './provider-command-discovery.js';
37
40
  import { providerCommandsMessage } from './provider-command-types.js';
41
+ import { copilotCapabilityService } from './copilot-capability-service.js';
42
+ import { mergeCopilotCapabilities } from './copilot-capabilities.js';
43
+ const copilotCapabilityRequests = new Map();
38
44
  import { claudeCapabilities, yeaftCapabilities } from './backends/types.js';
39
45
  import { createSessionSearchHandler } from './session-search-handler.js';
40
46
  import { formatSessionExport, isSessionExportFormat } from './session-export.js';
@@ -47,6 +53,7 @@ import { initScheduler, getRunningExecutions, } from './scheduler.js';
47
53
  import { handleWebUpgradeMessage } from './web-upgrade.js';
48
54
  import { refreshRuntimeLaunchSpecDeviceName } from './upgrade-launch-spec.js';
49
55
  import { getAgentRuntimeInfo } from './runtime-info.js';
56
+ import { listCopilotSessionsPageAsync, listAllCopilotSessionsPageAsync } from './runtime.js';
50
57
  let tunnelHandler = null;
51
58
  let terminalManager = null;
52
59
  const RECONNECT_BASE_DELAY = 1000;
@@ -103,6 +110,9 @@ function capabilitiesForBackend(backend, providerId) {
103
110
  })();
104
111
  if (!providerId)
105
112
  return capabilities;
113
+ if (backend === 'copilot' && ['copilot', 'agency-copilot'].includes(providerId)) {
114
+ capabilities.copilotOptions.catalog = mergeCopilotCapabilities(providerId);
115
+ }
106
116
  return {
107
117
  ...capabilities,
108
118
  commands: {
@@ -638,10 +648,13 @@ function deduped(key, fn) {
638
648
  });
639
649
  inflightHandlers.set(key, run());
640
650
  }
641
- export function send(msg) {
651
+ export function send(msg, expectedSocket) {
652
+ if (expectedSocket !== undefined && state.ws !== expectedSocket)
653
+ return;
642
654
  if (state.ws && state.ws.readyState === WebSocket.OPEN) {
643
655
  sendQueue = sendQueue.then(async () => {
644
- if (!state.ws || state.ws.readyState !== WebSocket.OPEN)
656
+ if (!state.ws || state.ws.readyState !== WebSocket.OPEN
657
+ || (expectedSocket !== undefined && state.ws !== expectedSocket))
645
658
  return;
646
659
  let payload = msg;
647
660
  // Don't E2E-encrypt messages the server must inspect (it only decrypts
@@ -662,6 +675,8 @@ export function send(msg) {
662
675
  if (state.e2eKey && state.e2eNegotiated && !skipE2E.includes(msg.type)) {
663
676
  payload = await e2eEncrypt(msg, state.e2eKey);
664
677
  }
678
+ if (expectedSocket !== undefined && state.ws !== expectedSocket)
679
+ return;
665
680
  return encryptAndSend(state.ws, payload, state.sessionKey);
666
681
  }).catch((err) => {
667
682
  console.error(`[AgentLink] send() failed for ${msg.type}:`, err.message);
@@ -981,17 +996,48 @@ function handleServerMessage(msg) {
981
996
  }
982
997
  else if (isCopilotBackend()) {
983
998
  const copilotMsg = msg;
984
- handleCopilotChat(chatConvId || '', copilotMsg.prompt, chatDir, {
999
+ void dispatchCopilotRequest({ conversationId: chatConvId || '', providerId: effectiveProviderId }, () => handleCopilotChat(chatConvId || '', copilotMsg.prompt, chatDir, {
985
1000
  resumeSessionId: chatOptions.resumeSessionId,
986
1001
  providerId: effectiveProviderId,
987
1002
  copilotOptions: copilotMsg.copilotOptions,
988
- }, copilotMsg.files);
1003
+ forkSourceSessionId: copilotMsg.forkSourceSessionId,
1004
+ }, copilotMsg.files), send, () => getCopilotConversation(chatConvId || '')?.turnActive === true);
989
1005
  }
990
1006
  else {
991
1007
  claudeHandleChat(chatConvId, msg.prompt, chatDir, chatOptions, msg.files);
992
1008
  }
993
1009
  break;
994
1010
  }
1011
+ case 'get_copilot_capabilities': {
1012
+ const m = msg;
1013
+ if (typeof m.requestId !== 'string' || !m.requestId || m.requestId.length > 200)
1014
+ break;
1015
+ if (m.cancel) {
1016
+ copilotCapabilityRequests.get(m.requestId)?.abort();
1017
+ break;
1018
+ }
1019
+ if (!['copilot', 'agency-copilot'].includes(m.providerId) || copilotCapabilityRequests.has(m.requestId))
1020
+ break;
1021
+ const scope = { providerId: m.providerId, workDir: path.resolve(m.workDir || state.workDir) };
1022
+ const controller = new AbortController();
1023
+ copilotCapabilityRequests.set(m.requestId, controller);
1024
+ send({ type: 'copilot_capabilities', requestId: m.requestId, providerId: m.providerId,
1025
+ workDir: scope.workDir, loading: true, catalog: copilotCapabilityService.snapshot(scope) });
1026
+ void copilotCapabilityService.query(scope, controller.signal).then(catalog => {
1027
+ if (!controller.signal.aborted)
1028
+ send({ type: 'copilot_capabilities', requestId: m.requestId,
1029
+ providerId: m.providerId, workDir: scope.workDir, loading: false, catalog });
1030
+ }).catch(error => {
1031
+ if (!controller.signal.aborted)
1032
+ send({ type: 'copilot_capabilities', requestId: m.requestId,
1033
+ providerId: m.providerId, workDir: scope.workDir, loading: false,
1034
+ catalog: { ...copilotCapabilityService.snapshot(scope), error: String(error) } });
1035
+ }).finally(() => {
1036
+ if (copilotCapabilityRequests.get(m.requestId) === controller)
1037
+ copilotCapabilityRequests.delete(m.requestId);
1038
+ });
1039
+ break;
1040
+ }
995
1041
  case 'get_provider_commands': {
996
1042
  const m = msg;
997
1043
  if (typeof m.requestId !== 'string' || !m.requestId.trim()
@@ -1014,6 +1060,7 @@ function handleServerMessage(msg) {
1014
1060
  providerId: m.providerId,
1015
1061
  conversationId: m.conversationId,
1016
1062
  workDir: path.resolve(m.workDir),
1063
+ ...(m.copilotOptions ? { copilotOptions: m.copilotOptions } : {}),
1017
1064
  };
1018
1065
  void discoverProviderCommands(scope)
1019
1066
  .then(result => send(providerCommandsMessage(scope, result)))
@@ -1023,6 +1070,32 @@ function handleServerMessage(msg) {
1023
1070
  })));
1024
1071
  break;
1025
1072
  }
1073
+ case 'execute_provider_command': {
1074
+ const m = msg;
1075
+ const conversation = m.conversationId
1076
+ ? getCopilotConversation(m.conversationId)
1077
+ : undefined;
1078
+ const providerId = m.providerId || conversation?.providerId || 'copilot';
1079
+ const command = m.command?.trim().toLowerCase();
1080
+ if (!m.conversationId
1081
+ || getBackendTypeForProvider(providerId) !== 'copilot'
1082
+ || !['compact', 'plan', 'goal'].includes(command || '')) {
1083
+ send({
1084
+ type: 'error',
1085
+ conversationId: m.conversationId,
1086
+ message: 'Invalid Copilot provider command request',
1087
+ });
1088
+ break;
1089
+ }
1090
+ const workDir = path.resolve(m.workDir || conversation?.workDir || state.workDir);
1091
+ const conversationId = m.conversationId;
1092
+ void dispatchCopilotRequest({ conversationId, providerId }, () => executeCopilotCommand(conversationId, command, typeof m.input === 'string' ? m.input : '', workDir, {
1093
+ providerId,
1094
+ resumeSessionId: resolveCopilotCommandResumeSessionId(m.resumeSessionId, conversation),
1095
+ copilotOptions: m.copilotOptions,
1096
+ }), send, () => getCopilotConversation(conversationId)?.turnActive === true);
1097
+ break;
1098
+ }
1026
1099
  case 'cancel_execution':
1027
1100
  {
1028
1101
  const conversationId = msg.conversationId || '';
@@ -1174,18 +1247,7 @@ function handleServerMessage(msg) {
1174
1247
  });
1175
1248
  }
1176
1249
  else if (recentBackend === 'copilot') {
1177
- const page = listAllCopilotSessionsPage({ limit: m.limit, cursor: m.cursor });
1178
- console.log(`[AgentLink] → recent_sessions_list (${page.sessions.length} copilot sessions) hasMore=${page.hasMore}`);
1179
- send({
1180
- type: 'recent_sessions_list',
1181
- sessions: page.sessions,
1182
- hasMore: page.hasMore,
1183
- nextCursor: page.nextCursor,
1184
- indexState: page.indexState,
1185
- ...(m.mode ? { mode: m.mode } : {}),
1186
- ...(m.requestId ? { requestId: m.requestId } : {}),
1187
- ...(m.providerId ? { providerId: m.providerId } : {}),
1188
- });
1250
+ void handleListRecentCopilotSessions(m);
1189
1251
  }
1190
1252
  else {
1191
1253
  handleListRecentSessions(m);
@@ -1418,6 +1480,10 @@ function handleServerMessage(msg) {
1418
1480
  }
1419
1481
  // Route to copilot backend
1420
1482
  if (resumeBackend === 'copilot') {
1483
+ const liveOwner = [...getCopilotConversations().values()].find(conv => conv.copilotSessionId === m.claudeSessionId || conv.lastCopilotSessionId === m.claudeSessionId);
1484
+ const resumeProviderId = liveOwner?.providerId || readCopilotSessionProvider(m.claudeSessionId)
1485
+ || m.providerId
1486
+ || 'copilot';
1421
1487
  // Copilot rewrites workspace.yaml using the cwd of each resume process.
1422
1488
  // Recover the persisted cwd before creating a placeholder so a reconnect
1423
1489
  // that omits workDir cannot silently move the session to the agent cwd.
@@ -1426,35 +1492,101 @@ function handleServerMessage(msg) {
1426
1492
  || state.workDir;
1427
1493
  // Rebind running copilot process to new conversationId (handles page refresh)
1428
1494
  if (convId) {
1495
+ const existing = getCopilotConversation(convId);
1496
+ const boundSession = existing?.copilotSessionId || existing?.lastCopilotSessionId;
1497
+ if (existing && (boundSession || existing.turnActive) && boundSession !== m.claudeSessionId) {
1498
+ send({ type: 'error', operation: 'resume_conversation', conversationId: convId,
1499
+ providerId: resumeProviderId, claudeSessionId: m.claudeSessionId,
1500
+ message: 'Copilot conversation/session mismatch. Reopen the intended conversation.' });
1501
+ break;
1502
+ }
1429
1503
  rebindCopilotConversation(m.claudeSessionId, convId);
1430
1504
  }
1431
1505
  // Create a copilot placeholder so follow-up chats auto-resume
1432
1506
  if (convId) {
1433
- createCopilotPlaceholder(convId, { providerId: m.providerId || 'copilot' });
1507
+ createCopilotPlaceholder(convId, { providerId: resumeProviderId });
1434
1508
  const placeholder = getCopilotConversation(convId);
1435
1509
  if (placeholder) {
1510
+ placeholder.providerId = resumeProviderId;
1436
1511
  placeholder.copilotSessionId = m.claudeSessionId;
1437
1512
  placeholder.workDir = resolvedWorkDir;
1438
1513
  }
1439
1514
  }
1440
1515
  const history = readCopilotSessionMessages(resolvedWorkDir, m.claudeSessionId);
1516
+ const persistedCopilotMode = readCopilotSessionMode(m.claudeSessionId);
1517
+ const persistedCopilotUsage = readCopilotSessionUsage(m.claudeSessionId);
1441
1518
  // Check if this session has an active copilot process running
1442
1519
  const copilotState = getCopilotConversation(convId || '');
1443
- const isCopilotProcessing = !!(copilotState?.turnActive && copilotState?.child);
1520
+ // A live persistent Runtime can be newer than events.jsonl while the
1521
+ // Provider is flushing its final usage checkpoint. Do not replace its
1522
+ // authoritative in-memory value merely because the page was reopened.
1523
+ const restoredCopilotUsage = copilotState?.lastUsage || persistedCopilotUsage;
1524
+ const isCopilotProcessing = !!copilotState?.turnActive;
1525
+ if (copilotState && !isCopilotProcessing) {
1526
+ copilotState.copilotMode = persistedCopilotMode;
1527
+ copilotState.planMode = persistedCopilotMode === 'plan';
1528
+ copilotState.planModePending = persistedCopilotMode === 'plan';
1529
+ copilotState.lastUsage = restoredCopilotUsage;
1530
+ copilotState.configuration = restoredCopilotUsage?.configuration;
1531
+ copilotState.model = restoredCopilotUsage?.configuration?.requested.model || restoredCopilotUsage?.model || null;
1532
+ const requestedContext = restoredCopilotUsage?.configuration?.requested.context || restoredCopilotUsage?.contextTier;
1533
+ const requestedEffort = restoredCopilotUsage?.configuration?.requested.effort || restoredCopilotUsage?.reasoningEffort;
1534
+ copilotState.contextTier = requestedContext === 'default' || requestedContext === 'long_context'
1535
+ ? requestedContext
1536
+ : null;
1537
+ copilotState.reasoningEffort = [
1538
+ 'none', 'low', 'medium', 'high', 'xhigh', 'max',
1539
+ ].includes(requestedEffort || '')
1540
+ ? requestedEffort
1541
+ : null;
1542
+ copilotState.autoCompactionPending = !!restoredCopilotUsage
1543
+ && Number.isFinite(restoredCopilotUsage.contextTokens)
1544
+ && Number.isFinite(restoredCopilotUsage.contextWindow)
1545
+ && Number(restoredCopilotUsage.contextWindow) > 0
1546
+ && Number(restoredCopilotUsage.contextTokens) / Number(restoredCopilotUsage.contextWindow) >= COPILOT_COMPACTION_POLICY.preRequest;
1547
+ }
1444
1548
  console.log(`[AgentLink] → conversation_resumed (${history.length} copilot messages, session ${m.claudeSessionId.slice(0, 8)})`);
1445
1549
  send({
1446
1550
  type: 'conversation_resumed',
1447
1551
  conversationId: convId,
1448
1552
  claudeSessionId: m.claudeSessionId,
1449
1553
  history,
1450
- isCompacting: false,
1554
+ isCompacting: copilotState?.isCompacting || false,
1555
+ compactions: [...(copilotState?.compactionRecords?.values() || [])].slice(-8),
1451
1556
  isProcessing: isCopilotProcessing,
1452
- planMode: false,
1557
+ turnId: copilotState?.turnId,
1558
+ stateVersion: copilotState?.stateVersion,
1559
+ planMode: copilotState?.planMode ?? (persistedCopilotMode === 'plan'),
1560
+ copilotMode: copilotState?.copilotMode || persistedCopilotMode,
1453
1561
  permissionMode: 'normal',
1454
1562
  workDir: resolvedWorkDir,
1563
+ providerId: resumeProviderId,
1564
+ ...(restoredCopilotUsage ? { usage: restoredCopilotUsage } : {}),
1455
1565
  ...(typeof m.scrollToMessageIdx === 'number' ? { scrollToMessageIdx: m.scrollToMessageIdx } : {}),
1456
1566
  ...(m.searchQuery ? { searchQuery: m.searchQuery } : {}),
1457
1567
  });
1568
+ // Seed the resolved history Provider's offline capabilities without
1569
+ // creating a metadata Runtime just because history was opened.
1570
+ send({ type: 'backend_info', conversationId: convId, backend: 'copilot', providerId: resumeProviderId,
1571
+ capabilities: capabilitiesForBackend('copilot', resumeProviderId) });
1572
+ if (convId && isCopilotProcessing) {
1573
+ for (const pending of getPendingCopilotDecisions(convId)) {
1574
+ send({
1575
+ type: 'backend_event',
1576
+ conversationId: convId,
1577
+ providerId: pending.providerId,
1578
+ claudeSessionId: pending.sessionId,
1579
+ turnId: copilotState?.turnId,
1580
+ stateVersion: copilotState?.stateVersion,
1581
+ event: {
1582
+ type: 'user_input_requested',
1583
+ session: { backendType: 'copilot', backendSessionId: pending.sessionId },
1584
+ requestId: pending.requestId,
1585
+ request: pending.request,
1586
+ },
1587
+ });
1588
+ }
1589
+ }
1458
1590
  break;
1459
1591
  }
1460
1592
  if (!convId) {
@@ -1553,10 +1685,22 @@ function handleServerMessage(msg) {
1553
1685
  }
1554
1686
  case 'ask_user_answer': {
1555
1687
  const m = msg;
1556
- if (isYeaftBackend())
1688
+ const copilotConversation = m.conversationId
1689
+ ? getCopilotConversation(m.conversationId)
1690
+ : undefined;
1691
+ if (copilotConversation
1692
+ || (m.providerId && getBackendTypeForProvider(m.providerId) === 'copilot')) {
1693
+ handleCopilotUserAnswer(m.requestId, m.answers, {
1694
+ conversationId: m.conversationId,
1695
+ providerId: m.providerId,
1696
+ });
1697
+ }
1698
+ else if (isYeaftBackend()) {
1557
1699
  handleYeaftUserAnswer(m.requestId, m.answers, { conversationId: m.conversationId });
1558
- else
1700
+ }
1701
+ else {
1559
1702
  handleUserAnswer(m.requestId, m.answers);
1703
+ }
1560
1704
  break;
1561
1705
  }
1562
1706
  case 'delete_session': {
@@ -1621,9 +1765,19 @@ function handleServerMessage(msg) {
1621
1765
  handleListCodexSessionsFn(dir);
1622
1766
  }
1623
1767
  else if (sessionBackend === 'copilot') {
1624
- const ok = deleteCopilotSession(dir, m.sessionId);
1625
- console.log(`[AgentLink] delete copilot session ${m.sessionId}: ${ok}`);
1626
- handleListCopilotSessionsFn(dir);
1768
+ void deleteCopilotSession(dir, m.sessionId).then(ok => {
1769
+ console.log(`[AgentLink] delete copilot session ${m.sessionId}: ${ok}`);
1770
+ if (ok)
1771
+ send({ type: 'session_deleted', sessionId: m.sessionId, providerId: m.providerId, workDir: dir });
1772
+ else
1773
+ send({ type: 'error', operation: 'delete_session', sessionId: m.sessionId,
1774
+ message: 'Session not found or could not be deleted.' });
1775
+ handleListCopilotSessionsFn(dir);
1776
+ }).catch(error => {
1777
+ send({ type: 'error', operation: 'delete_session', sessionId: m.sessionId,
1778
+ message: String(error.message || error) });
1779
+ handleListCopilotSessionsFn(dir);
1780
+ });
1627
1781
  }
1628
1782
  else {
1629
1783
  handleDeleteSession(m.sessionId, dir);
@@ -1856,10 +2010,12 @@ function handleServerMessage(msg) {
1856
2010
  const sessionId = conv.claudeSessionId ?? conv.copilotSessionId ?? conv.codexSessionId ?? conv.yeaftSessionId ?? conv.piSessionId ?? null;
1857
2011
  if (conv.turnActive) {
1858
2012
  active.push({
2013
+ providerId: conv.providerId, workDir: conv.workDir,
1859
2014
  conversationId: convId,
1860
2015
  claudeSessionId: sessionId,
1861
2016
  isProcessing: true,
1862
2017
  isCompacting: !!conv.isCompacting,
2018
+ ...(conv.compactionRecords ? { compactions: [...conv.compactionRecords.values()].slice(-8) } : {}),
1863
2019
  turnId: conv.turnId,
1864
2020
  stateVersion: conv.stateVersion,
1865
2021
  });
@@ -1868,12 +2024,14 @@ function handleServerMessage(msg) {
1868
2024
  // Include idle but resumable conversations so other tabs can
1869
2025
  // learn the backend session ID for multi-device continuity.
1870
2026
  active.push({
2027
+ providerId: conv.providerId, workDir: conv.workDir,
1871
2028
  conversationId: convId,
1872
2029
  claudeSessionId: sessionId,
1873
2030
  isProcessing: false,
1874
2031
  isCompacting: false,
1875
2032
  turnId: conv.turnId,
1876
2033
  stateVersion: conv.stateVersion,
2034
+ ...(conv.compactionRecords ? { compactions: [...conv.compactionRecords.values()].slice(-8) } : {}),
1877
2035
  });
1878
2036
  }
1879
2037
  };
@@ -1917,12 +2075,14 @@ function handleServerMessage(msg) {
1917
2075
  const status = isProc ? (isComp ? 'compacting' : 'processing') : 'idle';
1918
2076
  return {
1919
2077
  type: 'turn_status',
2078
+ providerId: conv.providerId, workDir: conv.workDir,
1920
2079
  conversationId: convId,
1921
2080
  claudeSessionId: conv.claudeSessionId ?? conv.copilotSessionId ?? conv.codexSessionId ?? conv.yeaftSessionId ?? conv.piSessionId ?? null,
1922
2081
  turnId: conv.turnId,
1923
2082
  status,
1924
2083
  isProcessing: isProc,
1925
2084
  isCompacting: isComp,
2085
+ ...(conv.compactionRecords ? { compactions: [...conv.compactionRecords.values()].slice(-8) } : {}),
1926
2086
  stateVersion: conv.stateVersion,
1927
2087
  };
1928
2088
  };
@@ -2114,8 +2274,26 @@ function handleServerMessage(msg) {
2114
2274
  break;
2115
2275
  }
2116
2276
  case 'set_plan_mode': {
2117
- const { enabled, conversationId } = msg;
2277
+ const { enabled, conversationId, providerId, workDir, copilotOptions, resumeSessionId } = msg;
2118
2278
  console.log(`[AgentLink] set_plan_mode: enabled=${enabled}, conversationId=${conversationId}`);
2279
+ const copilotConversation = conversationId
2280
+ ? getCopilotConversation(conversationId)
2281
+ : undefined;
2282
+ const effectiveProviderId = providerId || copilotConversation?.providerId;
2283
+ if (conversationId
2284
+ && (copilotConversation
2285
+ || (effectiveProviderId && getBackendTypeForProvider(effectiveProviderId) === 'copilot')
2286
+ || isCopilotBackend())) {
2287
+ void setCopilotPlanMode(conversationId, enabled, path.resolve(workDir || copilotConversation?.workDir || state.workDir), effectiveProviderId || 'copilot', copilotOptions, resumeSessionId).catch(error => {
2288
+ send({
2289
+ type: 'error',
2290
+ conversationId,
2291
+ message: error instanceof Error ? error.message : String(error),
2292
+ });
2293
+ send({ type: 'plan_mode_changed', enabled: !enabled, conversationId });
2294
+ });
2295
+ break;
2296
+ }
2119
2297
  const conv = getConversation(conversationId);
2120
2298
  if (conv) {
2121
2299
  // Restart with new mode
@@ -2131,6 +2309,34 @@ function handleServerMessage(msg) {
2131
2309
  send({ type: 'plan_mode_changed', enabled, conversationId });
2132
2310
  break;
2133
2311
  }
2312
+ case 'start_plan_implementation': {
2313
+ const { conversationId, planId, providerId, workDir, claudeSessionId, prompt, copilotOptions } = msg;
2314
+ if (!conversationId || !planId) {
2315
+ send({ type: 'error', conversationId, message: 'Invalid Plan implementation request' });
2316
+ break;
2317
+ }
2318
+ void startCopilotPlanImplementation(conversationId, planId, path.resolve(workDir || state.workDir), { providerId, resumeSessionId: claudeSessionId, prompt, copilotOptions }).catch(error => {
2319
+ send({
2320
+ type: 'backend_event',
2321
+ conversationId,
2322
+ providerId,
2323
+ claudeSessionId,
2324
+ event: {
2325
+ type: 'plan_implementation_status',
2326
+ session: { backendType: 'copilot', backendSessionId: claudeSessionId || '' },
2327
+ planId,
2328
+ status: 'failed',
2329
+ message: error instanceof Error ? error.message : String(error),
2330
+ },
2331
+ });
2332
+ send({
2333
+ type: 'error',
2334
+ conversationId,
2335
+ message: error instanceof Error ? error.message : String(error),
2336
+ });
2337
+ });
2338
+ break;
2339
+ }
2134
2340
  case 'set_permission_mode': {
2135
2341
  const { mode, conversationId } = msg;
2136
2342
  console.log(`[AgentLink] set_permission_mode: mode=${mode}, conversationId=${conversationId}`);
@@ -2189,7 +2395,7 @@ function handleServerMessage(msg) {
2189
2395
  break;
2190
2396
  }
2191
2397
  case 'set_provider': {
2192
- const { providerId, conversationId } = msg;
2398
+ let { providerId, conversationId, resumeSessionId, workDir, restoreOnly } = msg;
2193
2399
  console.log(`[AgentLink] set_provider: providerId=${providerId}, conversationId=${conversationId}`);
2194
2400
  // Detect backend type change
2195
2401
  const newBackendType = getBackendTypeForProvider(providerId);
@@ -2202,6 +2408,9 @@ function handleServerMessage(msg) {
2202
2408
  // are routed to background via the web's backgroundRouting handler.
2203
2409
  setBackendType(newBackendType);
2204
2410
  console.log(`[AgentLink] Backend switched: ${oldBackendType} → ${newBackendType}`);
2411
+ if (newBackendType === 'copilot' && conversationId) {
2412
+ providerId = setCopilotProvider(conversationId, providerId, { resumeSessionId, workDir, restoreOnly });
2413
+ }
2205
2414
  }
2206
2415
  else if (oldBackendType === 'yeaft') {
2207
2416
  const existingYeaftConv = conversationId ? getYeaftConversation(conversationId) : undefined;
@@ -2250,19 +2459,8 @@ function handleServerMessage(msg) {
2250
2459
  else if (oldBackendType === 'copilot') {
2251
2460
  // Same backend (copilot): provider sync during reconnect must not
2252
2461
  // restart an active subprocess.
2253
- const existingCopilotConv = conversationId ? getCopilotConversation(conversationId) : undefined;
2254
- if (existingCopilotConv?.turnActive) {
2255
- existingCopilotConv.providerId = providerId;
2256
- }
2257
- else if (conversationId && existingCopilotConv) {
2258
- const result = restartCopilotConversation(conversationId, { providerId });
2259
- if (result.wasTurnActive) {
2260
- send({ type: 'execution_cancelled', conversationId });
2261
- }
2262
- }
2263
- else if (conversationId) {
2264
- createCopilotPlaceholder(conversationId, { providerId });
2265
- }
2462
+ if (conversationId)
2463
+ providerId = setCopilotProvider(conversationId, providerId, { resumeSessionId, workDir, restoreOnly });
2266
2464
  }
2267
2465
  else {
2268
2466
  // Same backend (claude): reconnect provider sync must not interrupt
@@ -2281,7 +2479,7 @@ function handleServerMessage(msg) {
2281
2479
  createPlaceholderConversation(conversationId, { providerId });
2282
2480
  }
2283
2481
  }
2284
- send({ type: 'provider_changed', providerId, conversationId });
2482
+ send({ type: 'provider_changed', providerId, conversationId, ...(restoreOnly ? { restoreOnly: true } : {}) });
2285
2483
  // Capabilities are provider-scoped: wrappers and custom providers can
2286
2484
  // share a backend while exposing different command discovery behavior.
2287
2485
  send({
@@ -2847,18 +3045,24 @@ async function handleListSessions(dir, options = {}) {
2847
3045
  });
2848
3046
  }
2849
3047
  }
2850
- function handleListCopilotSessionsFn(dir, options = {}) {
3048
+ async function handleListCopilotSessionsFn(dir, options = {}) {
3049
+ const requestSocket = state.ws;
3050
+ const backend = getBackendType();
3051
+ const stillCurrent = () => state.ws === requestSocket && (!!options.providerId || getBackendType() === backend);
3052
+ const startedAt = performance.now();
2851
3053
  try {
2852
3054
  const page = options.legacy
2853
- ? { ...listCopilotSessionsPage(dir, { limit: Number.POSITIVE_INFINITY }), hasMore: false }
2854
- : listCopilotSessionsPage(dir, {
3055
+ ? { ...await listCopilotSessionsPageAsync(dir, { limit: Number.POSITIVE_INFINITY }), hasMore: false }
3056
+ : await listCopilotSessionsPageAsync(dir, {
2855
3057
  limit: options.limit ?? DEFAULT_SESSION_LIST_LIMIT,
2856
3058
  cursor: options.cursor,
2857
3059
  });
2858
3060
  if (options.legacy && page.sessions.length > LEGACY_SESSION_LIST_CAP) {
2859
3061
  page.sessions = page.sessions.slice(0, LEGACY_SESSION_LIST_CAP);
2860
3062
  }
2861
- console.log(`[AgentLink] → sessions_list (${page.sessions.length} copilot sessions for ${dir}) hasMore=${page.hasMore}`);
3063
+ if (!stillCurrent())
3064
+ return;
3065
+ console.log(`[AgentLink] → sessions_list (${page.sessions.length} copilot sessions for ${dir}) hasMore=${page.hasMore} durationMs=${Math.round(performance.now() - startedAt)}`);
2862
3066
  send({
2863
3067
  type: 'sessions_list',
2864
3068
  sessions: page.sessions,
@@ -2869,10 +3073,12 @@ function handleListCopilotSessionsFn(dir, options = {}) {
2869
3073
  ...(options.mode ? { mode: options.mode } : {}),
2870
3074
  ...(options.requestId ? { requestId: options.requestId } : {}),
2871
3075
  ...(options.providerId ? { providerId: options.providerId } : {}),
2872
- });
3076
+ }, requestSocket);
2873
3077
  }
2874
3078
  catch (err) {
2875
3079
  console.error(`[AgentLink] listCopilotSessions failed:`, err);
3080
+ if (!stillCurrent())
3081
+ return;
2876
3082
  send({
2877
3083
  type: 'sessions_list',
2878
3084
  sessions: [],
@@ -2883,7 +3089,32 @@ function handleListCopilotSessionsFn(dir, options = {}) {
2883
3089
  ...(options.mode ? { mode: options.mode } : {}),
2884
3090
  ...(options.requestId ? { requestId: options.requestId } : {}),
2885
3091
  ...(options.providerId ? { providerId: options.providerId } : {}),
2886
- });
3092
+ }, requestSocket);
3093
+ }
3094
+ }
3095
+ async function handleListRecentCopilotSessions(options) {
3096
+ const requestSocket = state.ws;
3097
+ const backend = getBackendType();
3098
+ const stillCurrent = () => state.ws === requestSocket && (!!options.providerId || getBackendType() === backend);
3099
+ const envelope = {
3100
+ type: 'recent_sessions_list',
3101
+ ...(options.mode ? { mode: options.mode } : {}),
3102
+ ...(options.requestId ? { requestId: options.requestId } : {}),
3103
+ ...(options.providerId ? { providerId: options.providerId } : {}),
3104
+ };
3105
+ const startedAt = performance.now();
3106
+ try {
3107
+ const page = await listAllCopilotSessionsPageAsync({ limit: options.limit, cursor: options.cursor });
3108
+ if (!stillCurrent())
3109
+ return;
3110
+ console.log(`[AgentLink] → recent_sessions_list (${page.sessions.length} copilot sessions) hasMore=${page.hasMore} durationMs=${Math.round(performance.now() - startedAt)}`);
3111
+ send({ ...envelope, ...page }, requestSocket);
3112
+ }
3113
+ catch (error) {
3114
+ console.error('[AgentLink] listAllCopilotSessions failed:', error);
3115
+ if (stillCurrent())
3116
+ send({ ...envelope, sessions: [], hasMore: false, indexState: 'ready',
3117
+ error: error instanceof Error ? error.message : String(error) }, requestSocket);
2887
3118
  }
2888
3119
  }
2889
3120
  function handleListCodexSessionsFn(dir, options = {}) {