@agent-link/agent 0.1.214 → 0.1.216

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.
@@ -19,8 +19,8 @@ import { createTunnelHandler } from './tunnel.js';
19
19
  import { createTerminalManager } from './terminal.js';
20
20
  const require = createRequire(import.meta.url);
21
21
  const pkg = require('../package.json');
22
- import { handleChat as claudeHandleChat, addSendFn, abort as abortClaude, abortAll as abortAllClaude, cancelExecution as claudeCancelExecution, handleBtwQuestion, getConversation, getConversations, clearSessionId, evictByClaudeSessionId, rebindConversation, addOutputObserver, removeOutputObserver, addCloseObserver, removeCloseObserver, setOutputObserver, clearOutputObserver, setCloseObserver, clearCloseObserver, getPendingQuestions, getPendingToolPermissions, addOnSessionStarted, getBackend, AgentRuntime } from './runtime.js';
23
- import { listAllRecentSessions, getSessionCacheStats } from './history.js';
22
+ 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 } from './runtime.js';
23
+ import { listSessions, readSessionMessages, deleteSession, renameSession, listAllRecentSessions, getSessionCacheStats } from './history.js';
24
24
  import { searchSessions } from './search-sessions.js';
25
25
  import { listMemoryFiles, updateMemoryFile, deleteMemoryFile } from './memory.js';
26
26
  import { decodeKey, parseMessage, encryptAndSend, e2eEncrypt, e2eDecrypt, isE2EEncrypted } from './encryption.js';
@@ -48,13 +48,6 @@ const state = {
48
48
  };
49
49
  let unsubscribeSend = null;
50
50
  let unsubscribeSessionStarted = null;
51
- /** PR4-B: connection-owned AgentRuntime. Shares the backend with the
52
- * process-wide getBackend() singleton until PR-W1 deletes the singleton. */
53
- let runtime = null;
54
- /** Test-only accessor for the active runtime instance. */
55
- export function _getRuntimeForTests() {
56
- return runtime;
57
- }
58
51
  let heartbeatInterval = null;
59
52
  let heartbeatTimeout = null;
60
53
  // Wire Entra timer module to this connection's state and send fn.
@@ -78,14 +71,6 @@ export function connect(config) {
78
71
  state.workDir = config.dir;
79
72
  state.config = config;
80
73
  state.shouldReconnect = true;
81
- // PR4-B: construct the connection-owned runtime. Pass getBackend() (NOT
82
- // createBackend()) so this runtime and the still-existing module singleton
83
- // share one ClaudeBackend instance — otherwise the addSendFn /
84
- // addOnSessionStarted re-exports below and the runtime would each register
85
- // on different backend instances and fan out events twice. PR-W1 deletes
86
- // getBackend(); at that point the runtime owns the backend exclusively.
87
- runtime = new AgentRuntime(getBackend());
88
- runtime.start();
89
74
  // Initialize E2E encryption key
90
75
  const ignoreConfig = !!process.env.AGENTLINK_NO_STATE;
91
76
  state.e2eKey = getOrCreateE2EKey(ignoreConfig);
@@ -392,13 +377,6 @@ export function disconnect() {
392
377
  state.ws.close();
393
378
  state.ws = null;
394
379
  }
395
- // PR4-B: tear down the connection-owned runtime. Awaited here so any future
396
- // backend (e.g. Codex) can do real async cleanup; the SIGINT/SIGTERM path
397
- // in index.ts uses shutdownSyncBestEffort() instead because it
398
- // process.exit(0)s immediately and cannot await.
399
- const r = runtime;
400
- runtime = null;
401
- return r ? r.shutdown() : Promise.resolve();
402
380
  }
403
381
  let sendQueue = Promise.resolve();
404
382
  export function send(msg) {
@@ -557,48 +535,14 @@ function handleServerMessage(msg) {
557
535
  if (actionItemId && chatConvId) {
558
536
  pendingActionItems.set(chatConvId, actionItemId);
559
537
  }
560
- // PR4-B: route through AgentRuntime. ClaudeBackend forwards `metadata`
561
- // (brainMode/recapId/briefingDate/devops*/projectName/icmId) as
562
- // HandleChatOptions to claude.handleChat. .catch() so any rejection
563
- // from ensureSession/backend.startTurn is logged rather than becoming
564
- // an unhandled rejection (Codex review #1, high). handleServerMessage
565
- // is sync, so we can't await; the caller in ws.on('message') has its
566
- // own try/catch but it would miss async rejections from this call.
567
- // Use 'default' as the conv key when missing — matches claude.ts's
568
- // DEFAULT_CONVERSATION_ID so the runtime map and backend's internal
569
- // map agree. Empty string would create a divergent mapping (Copilot
570
- // round 1).
571
- const startConvId = chatConvId ?? 'default';
572
- if (!runtime) {
573
- console.warn('[AgentLink] chat received after runtime shutdown — ignoring');
574
- break;
575
- }
576
- runtime.startTurn(startConvId, {
577
- text: msg.prompt,
578
- files: msg.files,
579
- metadata: chatOptions,
580
- }, { workDir: chatDir, resumeSessionId: chatOptions.resumeSessionId }).catch((err) => {
581
- console.error('[AgentLink] runtime.startTurn failed:', err.message);
582
- });
538
+ claudeHandleChat(chatConvId, msg.prompt, chatDir, chatOptions, msg.files);
583
539
  break;
584
540
  }
585
- case 'cancel_execution': {
586
- const cancelConvId = msg.conversationId;
587
- // 'default' default matches claude.ts (Copilot round 1).
588
- if (!runtime) {
589
- console.warn('[AgentLink] cancel received after runtime shutdown — ignoring');
590
- break;
591
- }
592
- runtime.interruptTurn(cancelConvId ?? 'default').catch((err) => {
593
- console.error('[AgentLink] runtime.interruptTurn failed:', err.message);
594
- });
541
+ case 'cancel_execution':
542
+ claudeCancelExecution(msg.conversationId);
595
543
  break;
596
- }
597
544
  case 'list_sessions':
598
- handleListSessions().catch((err) => {
599
- console.error('[AgentLink] handleListSessions failed:', err);
600
- send({ type: 'sessions_list', sessions: [], workDir: state.workDir });
601
- });
545
+ handleListSessions();
602
546
  break;
603
547
  case 'list_recent_sessions':
604
548
  handleListRecentSessions(msg);
@@ -663,10 +607,6 @@ function handleServerMessage(msg) {
663
607
  // Backward compat: old web client sends this to reset the single conversation
664
608
  abortClaude();
665
609
  clearSessionId('default');
666
- // Evict the runtime's cached ref so the next chat re-ensures (otherwise
667
- // ClaudeBackend would compute resumeSessionId from the stale ref and
668
- // resume the just-cleared session). Copilot round 3 fix.
669
- runtime?.evictConversation('default');
670
610
  console.log('[AgentLink] New conversation — session cleared');
671
611
  break;
672
612
  case 'resume_conversation': {
@@ -684,148 +624,101 @@ function handleServerMessage(msg) {
684
624
  // (handles page refresh where web client generates a new UUID)
685
625
  rebindConversation(m.claudeSessionId, convId);
686
626
  }
687
- // History reads went async in PR4-D (runtime.readHistory is async).
688
- // Wrap the rest of the handler in an async IIFE so we can await without
689
- // making the entire switch dispatch async. .catch() ensures backend
690
- // rejections don't become unhandled promise rejections.
691
- (async () => {
692
- // Try current workDir first; fall back to BRAIN_DATA_DIR for recap/briefing chat sessions
693
- let resolvedWorkDir = state.workDir;
694
- let history = runtime
695
- ? await runtime.readHistory(state.workDir, m.claudeSessionId)
696
- : [];
697
- if (history.length === 0) {
698
- const sessionMeta_ = loadSessionMetadata(m.claudeSessionId);
699
- if (sessionMeta_.recapId || sessionMeta_.briefingDate || sessionMeta_.devopsEntityType || sessionMeta_.projectName || sessionMeta_.icmId) {
700
- history = runtime
701
- ? await runtime.readHistory(BRAIN_DATA_DIR, m.claudeSessionId)
702
- : [];
703
- if (history.length > 0)
704
- resolvedWorkDir = BRAIN_DATA_DIR;
705
- }
706
- }
707
- // Fallback for BrainCore-spawned action-item sessions: try user home directory.
708
- // Action-item Claude sessions are spawned by BrainCore with cwd=$HOME and have
709
- // no AgentLink session-metadata sidecar, so neither lookup above finds them.
710
- if (history.length === 0) {
711
- history = runtime
712
- ? await runtime.readHistory(os.homedir(), m.claudeSessionId)
713
- : [];
627
+ // Try current workDir first; fall back to BRAIN_DATA_DIR for recap/briefing chat sessions
628
+ let resolvedWorkDir = state.workDir;
629
+ let history = readSessionMessages(state.workDir, m.claudeSessionId);
630
+ if (history.length === 0) {
631
+ const sessionMeta_ = loadSessionMetadata(m.claudeSessionId);
632
+ if (sessionMeta_.recapId || sessionMeta_.briefingDate || sessionMeta_.devopsEntityType || sessionMeta_.projectName || sessionMeta_.icmId) {
633
+ history = readSessionMessages(BRAIN_DATA_DIR, m.claudeSessionId);
714
634
  if (history.length > 0)
715
- resolvedWorkDir = os.homedir();
635
+ resolvedWorkDir = BRAIN_DATA_DIR;
716
636
  }
717
- console.log(`[AgentLink] → conversation_resumed (${history.length} messages, session ${m.claudeSessionId.slice(0, 8)})`);
718
- // Include live status so the web client can restore compacting/processing state
719
- // In multi-session mode, look up by conversationId; in single-session mode, use default
720
- const currentConv = convId ? getConversation(convId) : getConversation();
721
- const isSameSession = currentConv?.claudeSessionId === m.claudeSessionId
722
- || currentConv?.lastClaudeSessionId === m.claudeSessionId;
723
- // Restore persisted permission mode (falls back to 'normal' if not found)
724
- const persistedPermission = getConversationPermission(m.claudeSessionId) || 'normal';
725
- if (currentConv) {
726
- const currentMode = currentConv.permissionMode ?? (currentConv.planMode ? 'plan' : 'normal');
727
- const persistedMode = persistedPermission;
728
- const modeChanged = currentMode !== persistedMode;
729
- currentConv.planMode = persistedMode === 'plan';
730
- currentConv.permissionMode = persistedMode;
731
- // Only restart when the persisted mode differs from what's running,
732
- // and never interrupt an active turn.
733
- if (modeChanged && currentConv.child && !currentConv.turnActive) {
734
- const cid = convId ?? 'default';
735
- runtime?.restartConversation(cid, { planMode: currentConv.planMode, permissionMode: currentConv.permissionMode });
736
- // Re-seed runtime mapping with the resumed session id so the next
737
- // chat resumes m.claudeSessionId rather than starting fresh after
738
- // restartConversation evicts the cache. (Codex review round 1.)
739
- runtime?.ensureConversation(cid, { workDir: resolvedWorkDir, resumeSessionId: m.claudeSessionId }).catch(() => { });
740
- }
741
- // Remember the cwd the JSONL was found in so follow-up chats resume
742
- // in the same directory (Claude refuses --resume across cwds).
743
- currentConv.workDir = resolvedWorkDir;
637
+ }
638
+ // Fallback for BrainCore-spawned action-item sessions: try user home directory.
639
+ // Action-item Claude sessions are spawned by BrainCore with cwd=$HOME and have
640
+ // no AgentLink session-metadata sidecar, so neither lookup above finds them.
641
+ if (history.length === 0) {
642
+ history = readSessionMessages(os.homedir(), m.claudeSessionId);
643
+ if (history.length > 0)
644
+ resolvedWorkDir = os.homedir();
645
+ }
646
+ console.log(`[AgentLink] conversation_resumed (${history.length} messages, session ${m.claudeSessionId.slice(0, 8)})`);
647
+ // Include live status so the web client can restore compacting/processing state
648
+ // In multi-session mode, look up by conversationId; in single-session mode, use default
649
+ const currentConv = convId ? getConversation(convId) : getConversation();
650
+ const isSameSession = currentConv?.claudeSessionId === m.claudeSessionId
651
+ || currentConv?.lastClaudeSessionId === m.claudeSessionId;
652
+ // Restore persisted permission mode (falls back to 'normal' if not found)
653
+ const persistedPermission = getConversationPermission(m.claudeSessionId) || 'normal';
654
+ if (currentConv) {
655
+ const currentMode = currentConv.permissionMode ?? (currentConv.planMode ? 'plan' : 'normal');
656
+ const persistedMode = persistedPermission;
657
+ const modeChanged = currentMode !== persistedMode;
658
+ currentConv.planMode = persistedMode === 'plan';
659
+ currentConv.permissionMode = persistedMode;
660
+ // Only restart when the persisted mode differs from what's running,
661
+ // and never interrupt an active turn.
662
+ if (modeChanged && currentConv.child && !currentConv.turnActive) {
663
+ restartConversation(convId, { planMode: currentConv.planMode, permissionMode: currentConv.permissionMode });
744
664
  }
745
- // Always seed the runtime's convToSession map with the resumed session id
746
- // so the next `chat` for this conversation resumes the right Claude session
747
- // instead of spawning a fresh one. The web client only sends `resumeSessionId`
748
- // on the `resume_conversation` message — follow-up `chat` messages don't
749
- // re-send it, so the agent must remember it. Without this, ClaudeBackend's
750
- // ensureSession returns a `pending:` placeholder and startTurn drops the
751
- // resume id, causing each new message to start a new Claude session.
752
- // Skipped when there's already an active turn (would race with the live
753
- // process's own session id).
754
- const seedCid = convId ?? 'default';
755
- const seedConv = currentConv;
756
- if (!seedConv?.turnActive) {
757
- runtime?.ensureConversation(seedCid, {
758
- workDir: resolvedWorkDir,
759
- resumeSessionId: m.claudeSessionId,
760
- }).catch(() => { });
665
+ // Remember the cwd the JSONL was found in so follow-up chats resume
666
+ // in the same directory (Claude refuses --resume across cwds).
667
+ currentConv.workDir = resolvedWorkDir;
668
+ }
669
+ send({
670
+ type: 'conversation_resumed',
671
+ conversationId: convId,
672
+ claudeSessionId: m.claudeSessionId,
673
+ history,
674
+ isCompacting: isSameSession && (convId ? getIsCompacting(convId) : getIsCompacting()),
675
+ isProcessing: isSameSession && currentConv?.turnActive === true,
676
+ planMode: persistedPermission === 'plan',
677
+ permissionMode: persistedPermission,
678
+ ...(typeof m.scrollToMessageIdx === 'number' ? { scrollToMessageIdx: m.scrollToMessageIdx } : {}),
679
+ ...(m.searchQuery ? { searchQuery: m.searchQuery } : {}),
680
+ });
681
+ // Re-deliver any pending AskUserQuestion requests so the refreshed web client
682
+ // can display the question card and allow the user to answer.
683
+ if (convId && isSameSession) {
684
+ const pending = getPendingQuestions(convId);
685
+ for (const pq of pending) {
686
+ send({
687
+ type: 'ask_user_question',
688
+ conversationId: convId,
689
+ requestId: pq.requestId,
690
+ questions: pq.questions,
691
+ });
761
692
  }
762
- send({
763
- type: 'conversation_resumed',
764
- conversationId: convId,
765
- claudeSessionId: m.claudeSessionId,
766
- history,
767
- isCompacting: isSameSession && (runtime?.isCompacting(convId) ?? false),
768
- isProcessing: isSameSession && currentConv?.turnActive === true,
769
- planMode: persistedPermission === 'plan',
770
- permissionMode: persistedPermission,
771
- ...(typeof m.scrollToMessageIdx === 'number' ? { scrollToMessageIdx: m.scrollToMessageIdx } : {}),
772
- ...(m.searchQuery ? { searchQuery: m.searchQuery } : {}),
773
- });
774
- // Re-deliver any pending AskUserQuestion requests so the refreshed web client
775
- // can display the question card and allow the user to answer.
776
- if (convId && isSameSession) {
777
- const pending = getPendingQuestions(convId);
778
- for (const pq of pending) {
779
- send({
780
- type: 'ask_user_question',
781
- conversationId: convId,
782
- requestId: pq.requestId,
783
- questions: pq.questions,
784
- });
785
- }
786
- // Re-deliver pending tool permission requests (same pattern as ask_user_question)
787
- const pendingPerms = getPendingToolPermissions(convId);
788
- for (const pp of pendingPerms) {
789
- send({
790
- type: 'tool_permission_request',
791
- conversationId: convId,
792
- requestId: pp.requestId,
793
- toolName: pp.toolName,
794
- displayName: pp.displayName,
795
- input: pp.input,
796
- decisionReason: pp.decisionReason,
797
- });
798
- }
693
+ // Re-deliver pending tool permission requests (same pattern as ask_user_question)
694
+ const pendingPerms = getPendingToolPermissions(convId);
695
+ for (const pp of pendingPerms) {
696
+ send({
697
+ type: 'tool_permission_request',
698
+ conversationId: convId,
699
+ requestId: pp.requestId,
700
+ toolName: pp.toolName,
701
+ displayName: pp.displayName,
702
+ input: pp.input,
703
+ decisionReason: pp.decisionReason,
704
+ });
799
705
  }
800
- })().catch((err) => {
801
- console.error('[AgentLink] resume_conversation failed:', err);
802
- send({ type: 'error', message: 'Failed to resume conversation.' });
803
- });
706
+ }
804
707
  break;
805
708
  }
806
709
  case 'ask_user_answer': {
807
710
  const m = msg;
808
- if (!runtime) {
809
- console.warn('[AgentLink] ask_user_answer received after runtime shutdown — ignoring');
810
- break;
811
- }
812
- runtime.answerUserInput(m.requestId, m.answers);
711
+ handleUserAnswer(m.requestId, m.answers);
813
712
  break;
814
713
  }
815
714
  case 'delete_session': {
816
715
  const m = msg;
817
- handleDeleteSession(m.sessionId).catch((err) => {
818
- console.error('[AgentLink] handleDeleteSession failed:', err);
819
- send({ type: 'error', message: 'Session not found or could not be deleted.' });
820
- });
716
+ handleDeleteSession(m.sessionId);
821
717
  break;
822
718
  }
823
719
  case 'rename_session': {
824
720
  const m = msg;
825
- handleRenameSession(m.sessionId, m.newTitle).catch((err) => {
826
- console.error('[AgentLink] handleRenameSession failed:', err);
827
- send({ type: 'error', message: 'Session not found or could not be renamed.' });
828
- });
721
+ handleRenameSession(m.sessionId, m.newTitle);
829
722
  break;
830
723
  }
831
724
  case 'query_active_conversations': {
@@ -836,7 +729,7 @@ function handleServerMessage(msg) {
836
729
  conversationId: convId,
837
730
  claudeSessionId: conv.claudeSessionId,
838
731
  isProcessing: true,
839
- isCompacting: runtime?.isCompacting(convId) ?? false,
732
+ isCompacting: getIsCompacting(convId),
840
733
  });
841
734
  }
842
735
  else if (conv.claudeSessionId) {
@@ -950,11 +843,11 @@ function handleServerMessage(msg) {
950
843
  break;
951
844
  }
952
845
  console.log(`[AgentLink] set_model: model=${model}, conversationId=${conversationId}`);
953
- runtime?.setModel(conversationId ?? null, model);
846
+ setModel(conversationId, model);
954
847
  // Kill current Claude process so next message spawns with new --model flag
955
848
  const conv = getConversation(conversationId);
956
849
  if (conv) {
957
- runtime?.restartConversation(conversationId ?? 'default');
850
+ restartConversation(conversationId);
958
851
  }
959
852
  send({ type: 'model_changed', model, conversationId });
960
853
  break;
@@ -962,10 +855,17 @@ function handleServerMessage(msg) {
962
855
  case 'set_plan_mode': {
963
856
  const { enabled, conversationId } = msg;
964
857
  console.log(`[AgentLink] set_plan_mode: enabled=${enabled}, conversationId=${conversationId}`);
965
- const convId = conversationId ?? 'default';
966
- const result = runtime?.setPlanMode(convId, enabled, state.workDir) ?? null;
967
- if (result?.wasTurnActive) {
968
- send({ type: 'execution_cancelled', conversationId });
858
+ const conv = getConversation(conversationId);
859
+ if (conv) {
860
+ // Restart with new mode
861
+ const result = restartConversation(conversationId, { planMode: enabled });
862
+ if (result.wasTurnActive) {
863
+ send({ type: 'execution_cancelled', conversationId });
864
+ }
865
+ }
866
+ else {
867
+ // No conversation yet — create placeholder
868
+ createPlaceholderConversation(conversationId, { planMode: enabled });
969
869
  }
970
870
  send({ type: 'plan_mode_changed', enabled, conversationId });
971
871
  break;
@@ -973,10 +873,22 @@ function handleServerMessage(msg) {
973
873
  case 'set_permission_mode': {
974
874
  const { mode, conversationId } = msg;
975
875
  console.log(`[AgentLink] set_permission_mode: mode=${mode}, conversationId=${conversationId}`);
976
- const convId = conversationId ?? 'default';
977
- const result = runtime?.setPermissionMode(convId, mode, state.workDir) ?? null;
978
- if (result?.wasTurnActive) {
979
- send({ type: 'execution_cancelled', conversationId });
876
+ const planMode = mode === 'plan';
877
+ const conv = getConversation(conversationId);
878
+ if (conv) {
879
+ const result = restartConversation(conversationId, {
880
+ planMode,
881
+ permissionMode: mode,
882
+ });
883
+ if (result.wasTurnActive) {
884
+ send({ type: 'execution_cancelled', conversationId });
885
+ }
886
+ }
887
+ else {
888
+ createPlaceholderConversation(conversationId, {
889
+ planMode,
890
+ permissionMode: mode,
891
+ });
980
892
  }
981
893
  send({ type: 'permission_mode_changed', mode, conversationId });
982
894
  // Persist permission mode for this conversation's session
@@ -993,20 +905,23 @@ function handleServerMessage(msg) {
993
905
  case 'tool_permission_response': {
994
906
  const { requestId, behavior } = msg;
995
907
  console.log(`[AgentLink] tool_permission_response: ${behavior} (${requestId})`);
996
- if (!runtime) {
997
- console.warn('[AgentLink] tool_permission_response received after runtime shutdown — ignoring');
998
- break;
999
- }
1000
- runtime.answerApproval(requestId, behavior);
908
+ handleToolPermissionResponse(requestId, behavior);
1001
909
  break;
1002
910
  }
1003
911
  case 'set_brain_mode': {
1004
912
  const { enabled, conversationId } = msg;
1005
913
  console.log(`[AgentLink] set_brain_mode: enabled=${enabled}, conversationId=${conversationId}`);
1006
- const convId = conversationId ?? 'default';
1007
- const result = runtime?.setBrainMode(convId, enabled, state.workDir) ?? null;
1008
- if (result?.wasTurnActive) {
1009
- send({ type: 'execution_cancelled', conversationId });
914
+ const conv = getConversation(conversationId);
915
+ if (conv) {
916
+ // Restart with new brain mode
917
+ const result = restartConversation(conversationId, { brainMode: enabled });
918
+ if (result.wasTurnActive) {
919
+ send({ type: 'execution_cancelled', conversationId });
920
+ }
921
+ }
922
+ else {
923
+ // No conversation yet — create placeholder
924
+ createPlaceholderConversation(conversationId, { brainMode: enabled });
1010
925
  }
1011
926
  send({ type: 'brain_mode_changed', enabled, conversationId });
1012
927
  break;
@@ -1404,38 +1319,18 @@ async function handleBrainChatDeleteSession(msg) {
1404
1319
  send({ type: 'brainchat_error', message: String(err) });
1405
1320
  }
1406
1321
  }
1407
- async function handleListSessions() {
1322
+ function handleListSessions() {
1408
1323
  try {
1409
- if (!runtime) {
1410
- send({ type: 'sessions_list', sessions: [], workDir: state.workDir });
1411
- return;
1412
- }
1413
- // PR4-D: runtime.listHistory returns BackendSessionInfo rows; map to the
1414
- // flat wire shape connection.ts has always sent ({sessionId, title, ...}).
1415
- const rows = await runtime.listHistory(state.workDir);
1416
- const sessions = rows.map(r => ({
1417
- sessionId: r.session.backendSessionId,
1418
- title: r.title,
1419
- customTitle: r.customTitle,
1420
- preview: r.preview,
1421
- lastModified: r.lastModified,
1422
- }));
1324
+ const sessions = listSessions(state.workDir);
1423
1325
  const metaMap = loadAllSessionMetadata();
1424
1326
  const enriched = sessions.map(s => ({
1425
1327
  ...s,
1426
1328
  ...metaMap.get(s.sessionId),
1427
1329
  }));
1428
1330
  // Always merge recap/briefing/devops chat sessions from BrainData directory — they live under a
1429
- // different Claude project folder, so listHistory(state.workDir) misses them.
1331
+ // different Claude project folder, so listSessions(state.workDir) misses them.
1430
1332
  // These sessions should be visible regardless of the current workDir.
1431
- const brainRows = await runtime.listHistory(BRAIN_DATA_DIR);
1432
- const brainSessions = brainRows.map(r => ({
1433
- sessionId: r.session.backendSessionId,
1434
- title: r.title,
1435
- customTitle: r.customTitle,
1436
- preview: r.preview,
1437
- lastModified: r.lastModified,
1438
- }));
1333
+ const brainSessions = listSessions(BRAIN_DATA_DIR);
1439
1334
  const existingIds = new Set(enriched.map(s => s.sessionId));
1440
1335
  for (const bs of brainSessions) {
1441
1336
  if (existingIds.has(bs.sessionId))
@@ -1520,22 +1415,18 @@ function handleCancelSearch(msg) {
1520
1415
  searchControllers.delete(msg.searchId);
1521
1416
  }
1522
1417
  }
1523
- async function handleDeleteSession(sessionId) {
1418
+ function handleDeleteSession(sessionId) {
1524
1419
  // Evict any idle conversation holding this session; block if busy
1525
1420
  if (evictByClaudeSessionId(sessionId)) {
1526
1421
  send({ type: 'error', message: 'Cannot delete a session while it is processing.' });
1527
1422
  return;
1528
1423
  }
1529
- if (!runtime) {
1530
- send({ type: 'error', message: 'Session not found or could not be deleted.' });
1531
- return;
1532
- }
1533
1424
  // Try current workDir first; if not found, check if it's a recap/briefing/devops/project session in BrainData
1534
- let deleted = await runtime.deleteHistorySession(state.workDir, sessionId);
1425
+ let deleted = deleteSession(state.workDir, sessionId);
1535
1426
  if (!deleted) {
1536
1427
  const meta = loadSessionMetadata(sessionId);
1537
1428
  if (meta.recapId || meta.briefingDate || meta.devopsEntityType || meta.projectName || meta.icmId) {
1538
- deleted = await runtime.deleteHistorySession(BRAIN_DATA_DIR, sessionId);
1429
+ deleted = deleteSession(BRAIN_DATA_DIR, sessionId);
1539
1430
  }
1540
1431
  }
1541
1432
  if (deleted) {
@@ -1546,17 +1437,13 @@ async function handleDeleteSession(sessionId) {
1546
1437
  send({ type: 'error', message: 'Session not found or could not be deleted.' });
1547
1438
  }
1548
1439
  }
1549
- async function handleRenameSession(sessionId, newTitle) {
1550
- if (!runtime) {
1551
- send({ type: 'error', message: 'Session not found or could not be renamed.' });
1552
- return;
1553
- }
1440
+ function handleRenameSession(sessionId, newTitle) {
1554
1441
  // Try current workDir first; if not found, check if it's a recap/briefing/devops/project session in BrainData
1555
- let renamed = await runtime.renameHistorySession(state.workDir, sessionId, newTitle);
1442
+ let renamed = renameSession(state.workDir, sessionId, newTitle);
1556
1443
  if (!renamed) {
1557
1444
  const meta = loadSessionMetadata(sessionId);
1558
1445
  if (meta.recapId || meta.briefingDate || meta.devopsEntityType || meta.projectName || meta.icmId) {
1559
- renamed = await runtime.renameHistorySession(BRAIN_DATA_DIR, sessionId, newTitle);
1446
+ renamed = renameSession(BRAIN_DATA_DIR, sessionId, newTitle);
1560
1447
  }
1561
1448
  }
1562
1449
  if (renamed) {
@@ -1566,20 +1453,15 @@ async function handleRenameSession(sessionId, newTitle) {
1566
1453
  // Session file may not exist on disk yet (e.g. IcM/project chat still streaming first response).
1567
1454
  // Retry once after a short delay; if it still fails, send an error so the UI can recover.
1568
1455
  setTimeout(() => {
1569
- (async () => {
1570
- const retried = (await runtime.renameHistorySession(state.workDir, sessionId, newTitle))
1571
- || (await runtime.renameHistorySession(BRAIN_DATA_DIR, sessionId, newTitle));
1572
- if (retried) {
1573
- send({ type: 'session_renamed', sessionId, newTitle });
1574
- }
1575
- else {
1576
- console.warn(`[AgentLink] rename_session: session ${sessionId.slice(0, 8)} not found after retry`);
1577
- send({ type: 'error', message: 'Session not found or could not be renamed.' });
1578
- }
1579
- })().catch((err) => {
1580
- console.error('[AgentLink] rename_session retry failed:', err);
1456
+ const retried = renameSession(state.workDir, sessionId, newTitle)
1457
+ || renameSession(BRAIN_DATA_DIR, sessionId, newTitle);
1458
+ if (retried) {
1459
+ send({ type: 'session_renamed', sessionId, newTitle });
1460
+ }
1461
+ else {
1462
+ console.warn(`[AgentLink] rename_session: session ${sessionId.slice(0, 8)} not found after retry`);
1581
1463
  send({ type: 'error', message: 'Session not found or could not be renamed.' });
1582
- });
1464
+ }
1583
1465
  }, 1500);
1584
1466
  }
1585
1467
  }