@agent-link/agent 0.1.324 → 0.1.326

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.
@@ -10,7 +10,7 @@ import { handleCreateTeam, handleDissolveTeam, handleListTeams, handleGetTeam, h
10
10
  import { handleCreateLoop, handleUpdateLoop, handleDeleteLoop, handleListLoops, handleGetLoop, handleRunLoop, handleCancelLoopExecution, handleListLoopExecutions, handleGetLoopExecutionMessages, handleQueryLoopStatus } from './loop-handlers.js';
11
11
  import { loadSessionMetadata, loadAllSessionMetadata, deleteSessionMetadata } from './session-metadata.js';
12
12
  import { listRecaps, listRecapsWithStatus, getRecapDetail, runActionItemsCli } from './recap.js';
13
- import { listActions, getActionDetail } from './action.js';
13
+ import { listActions, getActionDetail, resolveActionCta } from './action.js';
14
14
  import { bindActionDraftSession, commitActionDraft, createActionDraft, discardActionDraft, getActionDraft, listActionDrafts, validateActionDraftId, } from './action-drafts.js';
15
15
  import { resolveNewActionDraftSessionDir } from './action-draft-session.js';
16
16
  import { listIcms } from './playground-icm.js';
@@ -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, 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';
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);
@@ -1259,7 +1403,27 @@ function handleServerMessage(msg) {
1259
1403
  const dir = m.workDir?.trim()
1260
1404
  ? path.resolve(m.workDir)
1261
1405
  : (m.providerId === 'brain' ? BRAIN_DATA_DIR : state.workDir);
1262
- 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') {
1263
1427
  if (evictByCodexSessionId(m.sessionId)) {
1264
1428
  send({ type: 'error', message: 'Cannot delete a session while it is processing.' });
1265
1429
  break;
@@ -1290,7 +1454,28 @@ function handleServerMessage(msg) {
1290
1454
  const dir = m.workDir?.trim()
1291
1455
  ? path.resolve(m.workDir)
1292
1456
  : (m.providerId === 'brain' ? BRAIN_DATA_DIR : state.workDir);
1293
- 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') {
1294
1479
  const ok = renameCodexSession(dir, m.sessionId, m.newTitle);
1295
1480
  console.log(`[AgentLink] rename codex session ${m.sessionId}: ${ok}`);
1296
1481
  if (ok)
@@ -1328,12 +1513,15 @@ function handleServerMessage(msg) {
1328
1513
  const sessionBackend = m.providerId
1329
1514
  ? getBackendTypeForProvider(m.providerId)
1330
1515
  : getBackendType();
1331
- if (hasActiveSessionConversation(sessionBackend, m.sessionId)) {
1516
+ if (hasActiveSessionConversation(sessionBackend, m.sessionId, m.providerId, fromDir)) {
1332
1517
  send({ type: 'error', message: 'Cannot move a session while it is processing.' });
1333
1518
  break;
1334
1519
  }
1335
1520
  let ok = false;
1336
- if (sessionBackend === 'codex') {
1521
+ if (sessionBackend === 'pi') {
1522
+ ok = movePiSession(fromDir, toDir, m.sessionId, m.providerId || 'pi');
1523
+ }
1524
+ else if (sessionBackend === 'codex') {
1337
1525
  ok = moveCodexSession(fromDir, toDir, m.sessionId);
1338
1526
  }
1339
1527
  else if (sessionBackend === 'copilot') {
@@ -1343,7 +1531,7 @@ function handleServerMessage(msg) {
1343
1531
  ok = moveSession(fromDir, toDir, m.sessionId);
1344
1532
  }
1345
1533
  if (ok) {
1346
- syncMovedConversationWorkDir(sessionBackend, m.sessionId, toDir);
1534
+ syncMovedConversationWorkDir(sessionBackend, m.sessionId, toDir, m.providerId, fromDir);
1347
1535
  if (sessionBackend === 'claude') {
1348
1536
  evictByClaudeSessionId(m.sessionId);
1349
1537
  }
@@ -1353,9 +1541,15 @@ function handleServerMessage(msg) {
1353
1541
  sessionId: m.sessionId,
1354
1542
  fromWorkDir: fromDir,
1355
1543
  toWorkDir: toDir,
1356
- ...(m.providerId ? { providerId: m.providerId } : {}),
1544
+ ...(sessionBackend === 'pi'
1545
+ ? { providerId: m.providerId || 'pi', workDir: toDir }
1546
+ : (m.providerId ? { providerId: m.providerId } : {})),
1357
1547
  });
1358
- 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') {
1359
1553
  handleListCodexSessionsFn(fromDir, { providerId: m.providerId });
1360
1554
  handleListCodexSessionsFn(toDir, { providerId: m.providerId });
1361
1555
  }
@@ -1385,7 +1579,13 @@ function handleServerMessage(msg) {
1385
1579
  break;
1386
1580
  }
1387
1581
  let readResult;
1388
- 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') {
1389
1589
  readResult = { history: readCodexSessionMessages(dir, m.sessionId), workDir: dir };
1390
1590
  }
1391
1591
  else if (exportBackend === 'copilot') {
@@ -1425,14 +1625,17 @@ function handleServerMessage(msg) {
1425
1625
  });
1426
1626
  break;
1427
1627
  }
1428
- sendSessionExportResult(requestId, m.sessionId, result);
1628
+ sendSessionExportResult(requestId, m.sessionId, result, {
1629
+ providerId: m.providerId || (exportBackend === 'pi' ? 'pi' : undefined),
1630
+ workDir: readResult.workDir,
1631
+ });
1429
1632
  break;
1430
1633
  }
1431
1634
  case 'query_active_conversations': {
1432
1635
  const request = msg;
1433
1636
  const active = [];
1434
1637
  const pushActive = (convId, conv) => {
1435
- const sessionId = conv.claudeSessionId ?? conv.copilotSessionId ?? conv.codexSessionId ?? null;
1638
+ const sessionId = conv.claudeSessionId ?? conv.copilotSessionId ?? conv.codexSessionId ?? conv.piSessionId ?? null;
1436
1639
  if (conv.turnActive) {
1437
1640
  active.push({
1438
1641
  conversationId: convId,
@@ -1465,6 +1668,9 @@ function handleServerMessage(msg) {
1465
1668
  for (const [convId, conv] of getCodexConversations()) {
1466
1669
  pushActive(convId, conv);
1467
1670
  }
1671
+ for (const [convId, conv] of getPiConversations()) {
1672
+ pushActive(convId, conv);
1673
+ }
1468
1674
  const activeTeamState = getActiveTeam();
1469
1675
  // Collect running loop executions for status recovery
1470
1676
  const runningLoops = [];
@@ -1491,7 +1697,7 @@ function handleServerMessage(msg) {
1491
1697
  return {
1492
1698
  type: 'turn_status',
1493
1699
  conversationId: convId,
1494
- claudeSessionId: conv.claudeSessionId ?? conv.copilotSessionId ?? conv.codexSessionId ?? null,
1700
+ claudeSessionId: conv.claudeSessionId ?? conv.copilotSessionId ?? conv.codexSessionId ?? conv.piSessionId ?? null,
1495
1701
  turnId: conv.turnId,
1496
1702
  status,
1497
1703
  isProcessing: isProc,
@@ -1526,6 +1732,15 @@ function handleServerMessage(msg) {
1526
1732
  }
1527
1733
  }
1528
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
+ }
1529
1744
  if (!found) {
1530
1745
  send({ type: 'turn_status', turnId: m.turnId, status: 'unknown', isProcessing: false, isCompacting: false, stateVersion: 0 });
1531
1746
  }
@@ -1546,7 +1761,13 @@ function handleServerMessage(msg) {
1546
1761
  send(buildTurnStatus(m.conversationId, codexConv));
1547
1762
  }
1548
1763
  else {
1549
- 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
+ }
1550
1771
  }
1551
1772
  }
1552
1773
  }
@@ -1746,6 +1967,21 @@ function handleServerMessage(msg) {
1746
1967
  setBackendType(newBackendType);
1747
1968
  console.log(`[AgentLink] Backend switched: ${oldBackendType} → ${newBackendType}`);
1748
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
+ }
1749
1985
  else if (oldBackendType === 'codex') {
1750
1986
  const existingCodexConv = conversationId ? getCodexConversation(conversationId) : undefined;
1751
1987
  if (existingCodexConv?.turnActive) {
@@ -1925,6 +2161,9 @@ function handleServerMessage(msg) {
1925
2161
  case 'get_action_detail':
1926
2162
  handleGetActionDetail(msg);
1927
2163
  break;
2164
+ case 'resolve_action_cta':
2165
+ handleResolveActionCta(msg);
2166
+ break;
1928
2167
  case 'complete_action_item':
1929
2168
  handleCompleteActionItem(msg);
1930
2169
  break;
@@ -2434,7 +2673,53 @@ function handleListCodexSessionsFn(dir, options = {}) {
2434
2673
  });
2435
2674
  }
2436
2675
  }
2437
- function sendSessionExportResult(requestId, sessionId, result) {
2676
+ function handleListPiSessionsFn(dir, options = {}) {
2677
+ try {
2678
+ const page = options.legacy
2679
+ ? {
2680
+ ...listPiSessionsPage(dir, {
2681
+ limit: Number.POSITIVE_INFINITY,
2682
+ providerId: options.providerId || 'pi',
2683
+ }),
2684
+ hasMore: false,
2685
+ }
2686
+ : listPiSessionsPage(dir, {
2687
+ limit: options.limit ?? DEFAULT_SESSION_LIST_LIMIT,
2688
+ cursor: options.cursor,
2689
+ providerId: options.providerId || 'pi',
2690
+ });
2691
+ if (options.legacy && page.sessions.length > LEGACY_SESSION_LIST_CAP) {
2692
+ page.sessions = page.sessions.slice(0, LEGACY_SESSION_LIST_CAP);
2693
+ }
2694
+ console.log(`[AgentLink] → sessions_list (${page.sessions.length} PI sessions for ${dir}) hasMore=${page.hasMore}`);
2695
+ send({
2696
+ type: 'sessions_list',
2697
+ sessions: page.sessions,
2698
+ workDir: dir,
2699
+ hasMore: page.hasMore,
2700
+ nextCursor: page.nextCursor,
2701
+ indexState: 'ready',
2702
+ ...(options.mode ? { mode: options.mode } : {}),
2703
+ ...(options.requestId ? { requestId: options.requestId } : {}),
2704
+ providerId: options.providerId || 'pi',
2705
+ });
2706
+ }
2707
+ catch (err) {
2708
+ console.error('[AgentLink] listPiSessions failed:', err);
2709
+ send({
2710
+ type: 'sessions_list',
2711
+ sessions: [],
2712
+ workDir: dir,
2713
+ hasMore: false,
2714
+ indexState: 'ready',
2715
+ error: err.message,
2716
+ ...(options.mode ? { mode: options.mode } : {}),
2717
+ ...(options.requestId ? { requestId: options.requestId } : {}),
2718
+ providerId: options.providerId || 'pi',
2719
+ });
2720
+ }
2721
+ }
2722
+ function sendSessionExportResult(requestId, sessionId, result, scope) {
2438
2723
  const totalChunks = Math.max(1, Math.ceil(result.content.length / SESSION_EXPORT_CHUNK_SIZE));
2439
2724
  console.log(`[AgentLink] → session_exported (${result.format}, ${Buffer.byteLength(result.content, 'utf8')} bytes, ${totalChunks} chunks)`);
2440
2725
  if (totalChunks === 1) {
@@ -2443,6 +2728,8 @@ function sendSessionExportResult(requestId, sessionId, result) {
2443
2728
  requestId,
2444
2729
  sessionId,
2445
2730
  ...result,
2731
+ ...(scope?.providerId ? { providerId: scope.providerId } : {}),
2732
+ ...(scope?.workDir ? { workDir: scope.workDir } : {}),
2446
2733
  });
2447
2734
  return;
2448
2735
  }
@@ -2458,6 +2745,8 @@ function sendSessionExportResult(requestId, sessionId, result) {
2458
2745
  chunkIndex,
2459
2746
  totalChunks,
2460
2747
  content: result.content.slice(start, start + SESSION_EXPORT_CHUNK_SIZE),
2748
+ ...(scope?.providerId ? { providerId: scope.providerId } : {}),
2749
+ ...(scope?.workDir ? { workDir: scope.workDir } : {}),
2461
2750
  });
2462
2751
  }
2463
2752
  }
@@ -2719,6 +3008,30 @@ async function handleGetActionDetail(msg) {
2719
3008
  send({ type: 'action_detail', actionItemId: msg.actionItemId, detail: null, error: String(err) });
2720
3009
  }
2721
3010
  }
3011
+ async function handleResolveActionCta(msg) {
3012
+ const intent = msg.intent || 'preview';
3013
+ try {
3014
+ const result = await resolveActionCta(msg.actionItemId, intent);
3015
+ console.log(`[AgentLink] → action_cta_resolved (${msg.actionItemId}, ${intent})`);
3016
+ send({
3017
+ type: 'action_cta_resolved',
3018
+ actionItemId: msg.actionItemId,
3019
+ requestId: msg.requestId,
3020
+ ...result,
3021
+ });
3022
+ }
3023
+ catch (err) {
3024
+ console.error(`[AgentLink] resolveActionCta failed for ${msg.actionItemId}:`, err);
3025
+ send({
3026
+ type: 'action_cta_resolved',
3027
+ actionItemId: msg.actionItemId,
3028
+ requestId: msg.requestId,
3029
+ intent,
3030
+ ok: false,
3031
+ error: String(err),
3032
+ });
3033
+ }
3034
+ }
2722
3035
  async function handleCompleteActionItem(msg) {
2723
3036
  try {
2724
3037
  await runActionItemsCli(['set-status', msg.actionItemId, 'completed', '--actor', 'portal', '--json']);