@agent-link/agent 0.1.323 → 0.1.325

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.
@@ -29,7 +29,8 @@ const require = createRequire(import.meta.url);
29
29
  const pkg = require('../package.json');
30
30
  const TEAM_SESSIONS_DIR = path.join(CONFIG_DIR, 'team-sessions');
31
31
  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, addCopilotSendFn, handleCopilotChat, cancelCopilotExecution, listCopilotSessionsPage, listAllCopilotSessionsPage, readCopilotSessionMessages, 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';
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, 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';
33
+ import { addPiSendFn, cancelPiExecution, cleanupAllPiConversations, createPiPlaceholder, deletePiSession, evictByPiSessionId, getPiConversation, getPiConversations, getPiSessionWorkDir, handlePiChat, listAllPiSessionsPage, listPiSessionsPage, movePiSession, piCapabilities, readPiSessionMessages, rebindPiConversation, renamePiSession, restartPiConversation, } from './pi.js';
33
34
  import { DEFAULT_SESSION_LIST_LIMIT, LEGACY_SESSION_LIST_CAP, listSessions, listSessionsPageAsync, readSessionMessages, deleteSession, renameSession, moveSession, listAllRecentSessionsPage, getSessionCacheStats } from './history.js';
34
35
  import { claudeCapabilities } from './backends/types.js';
35
36
  import { searchSessions } from './search-sessions.js';
@@ -69,6 +70,7 @@ const state = {
69
70
  let unsubscribeSend = null;
70
71
  let unsubscribeCopilotSend = null;
71
72
  let unsubscribeCodexSend = null;
73
+ let unsubscribePiSend = null;
72
74
  let unsubscribeSessionStarted = null;
73
75
  let heartbeatInterval = null;
74
76
  let heartbeatTimeout = null;
@@ -84,6 +86,8 @@ function capabilitiesForBackend(backend) {
84
86
  return copilotCapabilities();
85
87
  case 'codex':
86
88
  return codexCapabilities();
89
+ case 'pi':
90
+ return piCapabilities();
87
91
  case 'claude':
88
92
  default:
89
93
  return claudeCapabilities();
@@ -93,7 +97,7 @@ function normalizeDirForCompare(value) {
93
97
  const normalized = value.replace(/[\\/]+$/, '');
94
98
  return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
95
99
  }
96
- function hasActiveSessionConversation(backend, sessionId) {
100
+ function hasActiveSessionConversation(backend, sessionId, providerId, workDir) {
97
101
  if (backend === 'copilot') {
98
102
  for (const conv of getCopilotConversations().values()) {
99
103
  if (conv.copilotSessionId === sessionId || conv.lastCopilotSessionId === sessionId) {
@@ -110,6 +114,16 @@ function hasActiveSessionConversation(backend, sessionId) {
110
114
  }
111
115
  }
112
116
  }
117
+ else if (backend === 'pi') {
118
+ for (const conv of getPiConversations().values()) {
119
+ if ((conv.piSessionId === sessionId || conv.lastPiSessionId === sessionId)
120
+ && (!providerId || conv.providerId === providerId)
121
+ && (!workDir || normalizeDirForCompare(conv.workDir) === normalizeDirForCompare(workDir))) {
122
+ if (conv.turnActive)
123
+ return true;
124
+ }
125
+ }
126
+ }
113
127
  else {
114
128
  for (const conv of getConversations().values()) {
115
129
  if (conv.claudeSessionId === sessionId || conv.lastClaudeSessionId === sessionId) {
@@ -120,7 +134,7 @@ function hasActiveSessionConversation(backend, sessionId) {
120
134
  }
121
135
  return false;
122
136
  }
123
- function syncMovedConversationWorkDir(backend, sessionId, toWorkDir) {
137
+ function syncMovedConversationWorkDir(backend, sessionId, toWorkDir, providerId, fromWorkDir) {
124
138
  if (backend === 'copilot') {
125
139
  for (const conv of getCopilotConversations().values()) {
126
140
  if (conv.copilotSessionId === sessionId || conv.lastCopilotSessionId === sessionId)
@@ -133,6 +147,14 @@ function syncMovedConversationWorkDir(backend, sessionId, toWorkDir) {
133
147
  conv.workDir = toWorkDir;
134
148
  }
135
149
  }
150
+ else if (backend === 'pi') {
151
+ for (const conv of getPiConversations().values()) {
152
+ if ((conv.piSessionId === sessionId || conv.lastPiSessionId === sessionId)
153
+ && (!providerId || conv.providerId === providerId)
154
+ && (!fromWorkDir || normalizeDirForCompare(conv.workDir) === normalizeDirForCompare(fromWorkDir)))
155
+ conv.workDir = toWorkDir;
156
+ }
157
+ }
136
158
  else {
137
159
  for (const conv of getConversations().values()) {
138
160
  if (conv.claudeSessionId === sessionId || conv.lastClaudeSessionId === sessionId)
@@ -195,6 +217,13 @@ export function initializeLocalConnection(config) {
195
217
  catch { /* ignore */ }
196
218
  }
197
219
  unsubscribeCodexSend = addCodexSendFn(send);
220
+ if (unsubscribePiSend) {
221
+ try {
222
+ unsubscribePiSend();
223
+ }
224
+ catch { /* ignore */ }
225
+ }
226
+ unsubscribePiSend = addPiSendFn(send);
198
227
  // Wire up action item session linkage
199
228
  if (unsubscribeSessionStarted) {
200
229
  try {
@@ -492,6 +521,7 @@ export function disconnect() {
492
521
  abortAllClaude();
493
522
  cleanupAllCopilotConversations();
494
523
  cleanupAllCodexConversations();
524
+ cleanupAllPiConversations();
495
525
  if (unsubscribeSend) {
496
526
  try {
497
527
  unsubscribeSend();
@@ -513,6 +543,13 @@ export function disconnect() {
513
543
  catch { /* ignore */ }
514
544
  unsubscribeCodexSend = null;
515
545
  }
546
+ if (unsubscribePiSend) {
547
+ try {
548
+ unsubscribePiSend();
549
+ }
550
+ catch { /* ignore */ }
551
+ unsubscribePiSend = null;
552
+ }
516
553
  if (unsubscribeSessionStarted) {
517
554
  try {
518
555
  unsubscribeSessionStarted();
@@ -625,7 +662,8 @@ export function getConnectionStatus() {
625
662
  sessionRegistration: state.sessionRegistration,
626
663
  conversations: getConversations().size
627
664
  + getCopilotConversations().size
628
- + getCodexConversations().size,
665
+ + getCodexConversations().size
666
+ + getPiConversations().size,
629
667
  };
630
668
  }
631
669
  function buildWsUrl(config) {
@@ -854,7 +892,20 @@ function handleServerMessage(msg) {
854
892
  }
855
893
  }
856
894
  // Route to the selected backend.
857
- if (isCodexBackend()) {
895
+ if (isPiBackend()) {
896
+ const piMsg = msg;
897
+ const piState = chatConvId ? getPiConversation(chatConvId) : undefined;
898
+ const piResumeDir = chatOptions.resumeSessionId
899
+ ? getPiSessionWorkDir(chatOptions.resumeSessionId, effectiveProviderId || 'pi', explicitWorkDir)
900
+ : undefined;
901
+ const piWorkDir = explicitWorkDir || piState?.workDir || piResumeDir || chatDir;
902
+ handlePiChat(chatConvId || '', piMsg.prompt, piWorkDir, {
903
+ resumeSessionId: chatOptions.resumeSessionId,
904
+ providerId: effectiveProviderId,
905
+ piOptions: piMsg.piOptions,
906
+ }, piMsg.files);
907
+ }
908
+ else if (isCodexBackend()) {
858
909
  const codexMsg = msg;
859
910
  handleCodexChat(chatConvId || '', codexMsg.prompt, chatDir, {
860
911
  resumeSessionId: chatOptions.resumeSessionId,
@@ -876,14 +927,34 @@ function handleServerMessage(msg) {
876
927
  break;
877
928
  }
878
929
  case 'cancel_execution':
879
- if (isCodexBackend()) {
880
- cancelCodexExecution(msg.conversationId || '');
881
- }
882
- else if (isCopilotBackend()) {
883
- cancelCopilotExecution(msg.conversationId || '');
884
- }
885
- else {
886
- claudeCancelExecution(msg.conversationId);
930
+ {
931
+ const conversationId = msg.conversationId || '';
932
+ // Cancellation follows conversation ownership, not the currently selected
933
+ // provider. This keeps background turns cancellable after a tab switches.
934
+ if (getPiConversation(conversationId)) {
935
+ cancelPiExecution(conversationId);
936
+ }
937
+ else if (getCodexConversation(conversationId)) {
938
+ cancelCodexExecution(conversationId);
939
+ }
940
+ else if (getCopilotConversation(conversationId)) {
941
+ cancelCopilotExecution(conversationId);
942
+ }
943
+ else if (getConversation(conversationId)) {
944
+ claudeCancelExecution(conversationId);
945
+ }
946
+ else if (isPiBackend()) {
947
+ cancelPiExecution(conversationId);
948
+ }
949
+ else if (isCodexBackend()) {
950
+ cancelCodexExecution(conversationId);
951
+ }
952
+ else if (isCopilotBackend()) {
953
+ cancelCopilotExecution(conversationId);
954
+ }
955
+ else {
956
+ claudeCancelExecution(conversationId || undefined);
957
+ }
887
958
  }
888
959
  break;
889
960
  case 'list_sessions': {
@@ -893,7 +964,17 @@ function handleServerMessage(msg) {
893
964
  const sessionBackend = m.providerId
894
965
  ? getBackendTypeForProvider(m.providerId)
895
966
  : getBackendType();
896
- if (sessionBackend === 'codex') {
967
+ if (sessionBackend === 'pi') {
968
+ handleListPiSessionsFn(dir, {
969
+ limit: m.limit,
970
+ cursor: m.cursor,
971
+ mode: m.mode,
972
+ requestId: m.requestId,
973
+ providerId: m.providerId,
974
+ legacy: m.limit == null && !m.cursor && !m.mode && !m.requestId,
975
+ });
976
+ }
977
+ else if (sessionBackend === 'codex') {
897
978
  handleListCodexSessionsFn(dir, {
898
979
  limit: m.limit,
899
980
  cursor: m.cursor,
@@ -933,7 +1014,25 @@ function handleServerMessage(msg) {
933
1014
  const recentBackend = m.providerId
934
1015
  ? getBackendTypeForProvider(m.providerId)
935
1016
  : getBackendType();
936
- if (recentBackend === 'codex') {
1017
+ if (recentBackend === 'pi') {
1018
+ const page = listAllPiSessionsPage({
1019
+ limit: m.limit,
1020
+ cursor: m.cursor,
1021
+ providerId: m.providerId || 'pi',
1022
+ });
1023
+ console.log(`[AgentLink] → recent_sessions_list (${page.sessions.length} PI sessions) hasMore=${page.hasMore}`);
1024
+ send({
1025
+ type: 'recent_sessions_list',
1026
+ sessions: page.sessions,
1027
+ hasMore: page.hasMore,
1028
+ nextCursor: page.nextCursor,
1029
+ indexState: page.indexState,
1030
+ ...(m.mode ? { mode: m.mode } : {}),
1031
+ ...(m.requestId ? { requestId: m.requestId } : {}),
1032
+ providerId: m.providerId || 'pi',
1033
+ });
1034
+ }
1035
+ else if (recentBackend === 'codex') {
937
1036
  const page = listAllCodexSessionsPage({ limit: m.limit, cursor: m.cursor });
938
1037
  console.log(`[AgentLink] → recent_sessions_list (${page.sessions.length} codex sessions) hasMore=${page.hasMore}`);
939
1038
  send({
@@ -1080,6 +1179,51 @@ function handleServerMessage(msg) {
1080
1179
  const resumeBackend = m.providerId
1081
1180
  ? getBackendTypeForProvider(m.providerId)
1082
1181
  : getBackendType();
1182
+ if (resumeBackend === 'pi') {
1183
+ const persistedWorkDir = getPiSessionWorkDir(m.claudeSessionId, m.providerId || 'pi', explicitWorkDir);
1184
+ if (convId) {
1185
+ rebindPiConversation(m.claudeSessionId, convId, m.providerId || 'pi', explicitWorkDir || persistedWorkDir);
1186
+ }
1187
+ const reboundState = getPiConversation(convId || '');
1188
+ const authoritativeWorkDir = reboundState?.workDir || persistedWorkDir;
1189
+ const resolvedWorkDir = explicitWorkDir || authoritativeWorkDir || state.workDir;
1190
+ if (!authoritativeWorkDir
1191
+ || normalizeDirForCompare(authoritativeWorkDir) !== normalizeDirForCompare(resolvedWorkDir)) {
1192
+ send({
1193
+ type: 'error',
1194
+ message: `PI session ${m.claudeSessionId} was not found in ${resolvedWorkDir}.`,
1195
+ conversationId: convId,
1196
+ });
1197
+ break;
1198
+ }
1199
+ if (convId && !getPiConversation(convId)) {
1200
+ createPiPlaceholder(convId, { providerId: m.providerId || 'pi', workDir: resolvedWorkDir });
1201
+ }
1202
+ const piState = getPiConversation(convId || '');
1203
+ if (piState) {
1204
+ piState.piSessionId = m.claudeSessionId;
1205
+ piState.lastPiSessionId = m.claudeSessionId;
1206
+ piState.workDir = resolvedWorkDir;
1207
+ }
1208
+ const history = readPiSessionMessages(resolvedWorkDir, m.claudeSessionId, m.providerId || 'pi');
1209
+ const isPiProcessing = !!(piState?.turnActive && piState.child);
1210
+ console.log(`[AgentLink] → conversation_resumed (${history.length} PI messages, session ${m.claudeSessionId.slice(0, 8)})`);
1211
+ send({
1212
+ type: 'conversation_resumed',
1213
+ conversationId: convId,
1214
+ claudeSessionId: m.claudeSessionId,
1215
+ history,
1216
+ isCompacting: false,
1217
+ isProcessing: isPiProcessing,
1218
+ planMode: false,
1219
+ permissionMode: 'normal',
1220
+ workDir: resolvedWorkDir,
1221
+ providerId: m.providerId || 'pi',
1222
+ ...(typeof m.scrollToMessageIdx === 'number' ? { scrollToMessageIdx: m.scrollToMessageIdx } : {}),
1223
+ ...(m.searchQuery ? { searchQuery: m.searchQuery } : {}),
1224
+ });
1225
+ break;
1226
+ }
1083
1227
  if (resumeBackend === 'codex') {
1084
1228
  if (convId) {
1085
1229
  rebindCodexConversation(m.claudeSessionId, convId);
@@ -1113,6 +1257,12 @@ function handleServerMessage(msg) {
1113
1257
  }
1114
1258
  // Route to copilot backend
1115
1259
  if (resumeBackend === 'copilot') {
1260
+ // Copilot rewrites workspace.yaml using the cwd of each resume process.
1261
+ // Recover the persisted cwd before creating a placeholder so a reconnect
1262
+ // that omits workDir cannot silently move the session to the agent cwd.
1263
+ const resolvedWorkDir = explicitWorkDir
1264
+ || getCopilotSessionWorkDir(m.claudeSessionId)
1265
+ || state.workDir;
1116
1266
  // Rebind running copilot process to new conversationId (handles page refresh)
1117
1267
  if (convId) {
1118
1268
  rebindCopilotConversation(m.claudeSessionId, convId);
@@ -1123,10 +1273,9 @@ function handleServerMessage(msg) {
1123
1273
  const placeholder = getCopilotConversation(convId);
1124
1274
  if (placeholder) {
1125
1275
  placeholder.copilotSessionId = m.claudeSessionId;
1126
- placeholder.workDir = explicitWorkDir || state.workDir;
1276
+ placeholder.workDir = resolvedWorkDir;
1127
1277
  }
1128
1278
  }
1129
- const resolvedWorkDir = explicitWorkDir || state.workDir;
1130
1279
  const history = readCopilotSessionMessages(resolvedWorkDir, m.claudeSessionId);
1131
1280
  // Check if this session has an active copilot process running
1132
1281
  const copilotState = getCopilotConversation(convId || '');
@@ -1141,6 +1290,7 @@ function handleServerMessage(msg) {
1141
1290
  isProcessing: isCopilotProcessing,
1142
1291
  planMode: false,
1143
1292
  permissionMode: 'normal',
1293
+ workDir: resolvedWorkDir,
1144
1294
  ...(typeof m.scrollToMessageIdx === 'number' ? { scrollToMessageIdx: m.scrollToMessageIdx } : {}),
1145
1295
  ...(m.searchQuery ? { searchQuery: m.searchQuery } : {}),
1146
1296
  });
@@ -1253,7 +1403,27 @@ function handleServerMessage(msg) {
1253
1403
  const dir = m.workDir?.trim()
1254
1404
  ? path.resolve(m.workDir)
1255
1405
  : (m.providerId === 'brain' ? BRAIN_DATA_DIR : state.workDir);
1256
- if (sessionBackend === 'codex') {
1406
+ if (sessionBackend === 'pi') {
1407
+ if (evictByPiSessionId(m.sessionId, m.providerId || 'pi', dir)) {
1408
+ send({ type: 'error', message: 'Cannot delete a session while it is processing.' });
1409
+ break;
1410
+ }
1411
+ const ok = deletePiSession(dir, m.sessionId, m.providerId || 'pi');
1412
+ console.log(`[AgentLink] delete PI session ${m.sessionId}: ${ok}`);
1413
+ if (ok) {
1414
+ send({
1415
+ type: 'session_deleted',
1416
+ sessionId: m.sessionId,
1417
+ providerId: m.providerId || 'pi',
1418
+ workDir: dir,
1419
+ });
1420
+ }
1421
+ else {
1422
+ send({ type: 'error', message: 'Session not found or could not be deleted.' });
1423
+ }
1424
+ handleListPiSessionsFn(dir, { providerId: m.providerId || 'pi' });
1425
+ }
1426
+ else if (sessionBackend === 'codex') {
1257
1427
  if (evictByCodexSessionId(m.sessionId)) {
1258
1428
  send({ type: 'error', message: 'Cannot delete a session while it is processing.' });
1259
1429
  break;
@@ -1284,7 +1454,28 @@ function handleServerMessage(msg) {
1284
1454
  const dir = m.workDir?.trim()
1285
1455
  ? path.resolve(m.workDir)
1286
1456
  : (m.providerId === 'brain' ? BRAIN_DATA_DIR : state.workDir);
1287
- if (sessionBackend === 'codex') {
1457
+ if (sessionBackend === 'pi') {
1458
+ if (hasActiveSessionConversation('pi', m.sessionId, m.providerId || 'pi', dir)) {
1459
+ send({ type: 'error', message: 'Cannot rename a session while it is processing.' });
1460
+ break;
1461
+ }
1462
+ const ok = renamePiSession(dir, m.sessionId, m.newTitle, m.providerId || 'pi');
1463
+ console.log(`[AgentLink] rename PI session ${m.sessionId}: ${ok}`);
1464
+ if (ok) {
1465
+ send({
1466
+ type: 'session_renamed',
1467
+ sessionId: m.sessionId,
1468
+ newTitle: m.newTitle,
1469
+ providerId: m.providerId || 'pi',
1470
+ workDir: dir,
1471
+ });
1472
+ }
1473
+ else {
1474
+ send({ type: 'error', message: 'Session not found or could not be renamed.' });
1475
+ }
1476
+ handleListPiSessionsFn(dir, { providerId: m.providerId || 'pi' });
1477
+ }
1478
+ else if (sessionBackend === 'codex') {
1288
1479
  const ok = renameCodexSession(dir, m.sessionId, m.newTitle);
1289
1480
  console.log(`[AgentLink] rename codex session ${m.sessionId}: ${ok}`);
1290
1481
  if (ok)
@@ -1322,12 +1513,15 @@ function handleServerMessage(msg) {
1322
1513
  const sessionBackend = m.providerId
1323
1514
  ? getBackendTypeForProvider(m.providerId)
1324
1515
  : getBackendType();
1325
- if (hasActiveSessionConversation(sessionBackend, m.sessionId)) {
1516
+ if (hasActiveSessionConversation(sessionBackend, m.sessionId, m.providerId, fromDir)) {
1326
1517
  send({ type: 'error', message: 'Cannot move a session while it is processing.' });
1327
1518
  break;
1328
1519
  }
1329
1520
  let ok = false;
1330
- if (sessionBackend === 'codex') {
1521
+ if (sessionBackend === 'pi') {
1522
+ ok = movePiSession(fromDir, toDir, m.sessionId, m.providerId || 'pi');
1523
+ }
1524
+ else if (sessionBackend === 'codex') {
1331
1525
  ok = moveCodexSession(fromDir, toDir, m.sessionId);
1332
1526
  }
1333
1527
  else if (sessionBackend === 'copilot') {
@@ -1337,7 +1531,7 @@ function handleServerMessage(msg) {
1337
1531
  ok = moveSession(fromDir, toDir, m.sessionId);
1338
1532
  }
1339
1533
  if (ok) {
1340
- syncMovedConversationWorkDir(sessionBackend, m.sessionId, toDir);
1534
+ syncMovedConversationWorkDir(sessionBackend, m.sessionId, toDir, m.providerId, fromDir);
1341
1535
  if (sessionBackend === 'claude') {
1342
1536
  evictByClaudeSessionId(m.sessionId);
1343
1537
  }
@@ -1347,9 +1541,15 @@ function handleServerMessage(msg) {
1347
1541
  sessionId: m.sessionId,
1348
1542
  fromWorkDir: fromDir,
1349
1543
  toWorkDir: toDir,
1350
- ...(m.providerId ? { providerId: m.providerId } : {}),
1544
+ ...(sessionBackend === 'pi'
1545
+ ? { providerId: m.providerId || 'pi', workDir: toDir }
1546
+ : (m.providerId ? { providerId: m.providerId } : {})),
1351
1547
  });
1352
- if (sessionBackend === 'codex') {
1548
+ if (sessionBackend === 'pi') {
1549
+ handleListPiSessionsFn(fromDir, { providerId: m.providerId || 'pi' });
1550
+ handleListPiSessionsFn(toDir, { providerId: m.providerId || 'pi' });
1551
+ }
1552
+ else if (sessionBackend === 'codex') {
1353
1553
  handleListCodexSessionsFn(fromDir, { providerId: m.providerId });
1354
1554
  handleListCodexSessionsFn(toDir, { providerId: m.providerId });
1355
1555
  }
@@ -1379,7 +1579,13 @@ function handleServerMessage(msg) {
1379
1579
  break;
1380
1580
  }
1381
1581
  let readResult;
1382
- if (exportBackend === 'codex') {
1582
+ if (exportBackend === 'pi') {
1583
+ readResult = {
1584
+ history: readPiSessionMessages(dir, m.sessionId, m.providerId || 'pi'),
1585
+ workDir: dir,
1586
+ };
1587
+ }
1588
+ else if (exportBackend === 'codex') {
1383
1589
  readResult = { history: readCodexSessionMessages(dir, m.sessionId), workDir: dir };
1384
1590
  }
1385
1591
  else if (exportBackend === 'copilot') {
@@ -1419,14 +1625,17 @@ function handleServerMessage(msg) {
1419
1625
  });
1420
1626
  break;
1421
1627
  }
1422
- sendSessionExportResult(requestId, m.sessionId, result);
1628
+ sendSessionExportResult(requestId, m.sessionId, result, {
1629
+ providerId: m.providerId || (exportBackend === 'pi' ? 'pi' : undefined),
1630
+ workDir: readResult.workDir,
1631
+ });
1423
1632
  break;
1424
1633
  }
1425
1634
  case 'query_active_conversations': {
1426
1635
  const request = msg;
1427
1636
  const active = [];
1428
1637
  const pushActive = (convId, conv) => {
1429
- const sessionId = conv.claudeSessionId ?? conv.copilotSessionId ?? conv.codexSessionId ?? null;
1638
+ const sessionId = conv.claudeSessionId ?? conv.copilotSessionId ?? conv.codexSessionId ?? conv.piSessionId ?? null;
1430
1639
  if (conv.turnActive) {
1431
1640
  active.push({
1432
1641
  conversationId: convId,
@@ -1459,6 +1668,9 @@ function handleServerMessage(msg) {
1459
1668
  for (const [convId, conv] of getCodexConversations()) {
1460
1669
  pushActive(convId, conv);
1461
1670
  }
1671
+ for (const [convId, conv] of getPiConversations()) {
1672
+ pushActive(convId, conv);
1673
+ }
1462
1674
  const activeTeamState = getActiveTeam();
1463
1675
  // Collect running loop executions for status recovery
1464
1676
  const runningLoops = [];
@@ -1485,7 +1697,7 @@ function handleServerMessage(msg) {
1485
1697
  return {
1486
1698
  type: 'turn_status',
1487
1699
  conversationId: convId,
1488
- claudeSessionId: conv.claudeSessionId ?? conv.copilotSessionId ?? conv.codexSessionId ?? null,
1700
+ claudeSessionId: conv.claudeSessionId ?? conv.copilotSessionId ?? conv.codexSessionId ?? conv.piSessionId ?? null,
1489
1701
  turnId: conv.turnId,
1490
1702
  status,
1491
1703
  isProcessing: isProc,
@@ -1520,6 +1732,15 @@ function handleServerMessage(msg) {
1520
1732
  }
1521
1733
  }
1522
1734
  }
1735
+ if (!found) {
1736
+ for (const [convId, conv] of getPiConversations()) {
1737
+ if (conv.turnId === m.turnId) {
1738
+ send(buildTurnStatus(convId, conv));
1739
+ found = true;
1740
+ break;
1741
+ }
1742
+ }
1743
+ }
1523
1744
  if (!found) {
1524
1745
  send({ type: 'turn_status', turnId: m.turnId, status: 'unknown', isProcessing: false, isCompacting: false, stateVersion: 0 });
1525
1746
  }
@@ -1540,7 +1761,13 @@ function handleServerMessage(msg) {
1540
1761
  send(buildTurnStatus(m.conversationId, codexConv));
1541
1762
  }
1542
1763
  else {
1543
- send({ type: 'turn_status', conversationId: m.conversationId, status: 'unknown', isProcessing: false, isCompacting: false, stateVersion: 0 });
1764
+ const piConv = getPiConversation(m.conversationId);
1765
+ if (piConv) {
1766
+ send(buildTurnStatus(m.conversationId, piConv));
1767
+ }
1768
+ else {
1769
+ send({ type: 'turn_status', conversationId: m.conversationId, status: 'unknown', isProcessing: false, isCompacting: false, stateVersion: 0 });
1770
+ }
1544
1771
  }
1545
1772
  }
1546
1773
  }
@@ -1740,6 +1967,21 @@ function handleServerMessage(msg) {
1740
1967
  setBackendType(newBackendType);
1741
1968
  console.log(`[AgentLink] Backend switched: ${oldBackendType} → ${newBackendType}`);
1742
1969
  }
1970
+ else if (oldBackendType === 'pi') {
1971
+ const existingPiConversation = conversationId ? getPiConversation(conversationId) : undefined;
1972
+ if (existingPiConversation?.turnActive) {
1973
+ // Keep the live process bound to the provider that launched it.
1974
+ // The next chat carries the new providerId and restarts PI after settlement.
1975
+ }
1976
+ else if (conversationId && existingPiConversation) {
1977
+ const result = restartPiConversation(conversationId, { providerId });
1978
+ if (result.wasTurnActive)
1979
+ send({ type: 'execution_cancelled', conversationId });
1980
+ }
1981
+ else if (conversationId) {
1982
+ createPiPlaceholder(conversationId, { providerId });
1983
+ }
1984
+ }
1743
1985
  else if (oldBackendType === 'codex') {
1744
1986
  const existingCodexConv = conversationId ? getCodexConversation(conversationId) : undefined;
1745
1987
  if (existingCodexConv?.turnActive) {
@@ -2428,7 +2670,53 @@ function handleListCodexSessionsFn(dir, options = {}) {
2428
2670
  });
2429
2671
  }
2430
2672
  }
2431
- function sendSessionExportResult(requestId, sessionId, result) {
2673
+ function handleListPiSessionsFn(dir, options = {}) {
2674
+ try {
2675
+ const page = options.legacy
2676
+ ? {
2677
+ ...listPiSessionsPage(dir, {
2678
+ limit: Number.POSITIVE_INFINITY,
2679
+ providerId: options.providerId || 'pi',
2680
+ }),
2681
+ hasMore: false,
2682
+ }
2683
+ : listPiSessionsPage(dir, {
2684
+ limit: options.limit ?? DEFAULT_SESSION_LIST_LIMIT,
2685
+ cursor: options.cursor,
2686
+ providerId: options.providerId || 'pi',
2687
+ });
2688
+ if (options.legacy && page.sessions.length > LEGACY_SESSION_LIST_CAP) {
2689
+ page.sessions = page.sessions.slice(0, LEGACY_SESSION_LIST_CAP);
2690
+ }
2691
+ console.log(`[AgentLink] → sessions_list (${page.sessions.length} PI sessions for ${dir}) hasMore=${page.hasMore}`);
2692
+ send({
2693
+ type: 'sessions_list',
2694
+ sessions: page.sessions,
2695
+ workDir: dir,
2696
+ hasMore: page.hasMore,
2697
+ nextCursor: page.nextCursor,
2698
+ indexState: 'ready',
2699
+ ...(options.mode ? { mode: options.mode } : {}),
2700
+ ...(options.requestId ? { requestId: options.requestId } : {}),
2701
+ providerId: options.providerId || 'pi',
2702
+ });
2703
+ }
2704
+ catch (err) {
2705
+ console.error('[AgentLink] listPiSessions failed:', err);
2706
+ send({
2707
+ type: 'sessions_list',
2708
+ sessions: [],
2709
+ workDir: dir,
2710
+ hasMore: false,
2711
+ indexState: 'ready',
2712
+ error: err.message,
2713
+ ...(options.mode ? { mode: options.mode } : {}),
2714
+ ...(options.requestId ? { requestId: options.requestId } : {}),
2715
+ providerId: options.providerId || 'pi',
2716
+ });
2717
+ }
2718
+ }
2719
+ function sendSessionExportResult(requestId, sessionId, result, scope) {
2432
2720
  const totalChunks = Math.max(1, Math.ceil(result.content.length / SESSION_EXPORT_CHUNK_SIZE));
2433
2721
  console.log(`[AgentLink] → session_exported (${result.format}, ${Buffer.byteLength(result.content, 'utf8')} bytes, ${totalChunks} chunks)`);
2434
2722
  if (totalChunks === 1) {
@@ -2437,6 +2725,8 @@ function sendSessionExportResult(requestId, sessionId, result) {
2437
2725
  requestId,
2438
2726
  sessionId,
2439
2727
  ...result,
2728
+ ...(scope?.providerId ? { providerId: scope.providerId } : {}),
2729
+ ...(scope?.workDir ? { workDir: scope.workDir } : {}),
2440
2730
  });
2441
2731
  return;
2442
2732
  }
@@ -2452,6 +2742,8 @@ function sendSessionExportResult(requestId, sessionId, result) {
2452
2742
  chunkIndex,
2453
2743
  totalChunks,
2454
2744
  content: result.content.slice(start, start + SESSION_EXPORT_CHUNK_SIZE),
2745
+ ...(scope?.providerId ? { providerId: scope.providerId } : {}),
2746
+ ...(scope?.workDir ? { workDir: scope.workDir } : {}),
2455
2747
  });
2456
2748
  }
2457
2749
  }