@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.
- package/dist/backends/claude.d.ts +2 -8
- package/dist/backends/claude.js +2 -47
- package/dist/backends/claude.js.map +1 -1
- package/dist/backends/types.d.ts +2 -60
- package/dist/backends/types.js +0 -16
- package/dist/backends/types.js.map +1 -1
- package/dist/connection.d.ts +1 -4
- package/dist/connection.js +144 -262
- package/dist/connection.js.map +1 -1
- package/dist/index.js +1 -8
- package/dist/index.js.map +1 -1
- package/dist/runtime.d.ts +1 -129
- package/dist/runtime.js +1 -220
- package/dist/runtime.js.map +1 -1
- package/package.json +1 -1
package/dist/connection.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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()
|
|
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
|
-
//
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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 =
|
|
635
|
+
resolvedWorkDir = BRAIN_DATA_DIR;
|
|
716
636
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
currentConv.
|
|
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
|
-
//
|
|
746
|
-
//
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
|
977
|
-
const
|
|
978
|
-
if (
|
|
979
|
-
|
|
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
|
-
|
|
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
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
-
|
|
1322
|
+
function handleListSessions() {
|
|
1408
1323
|
try {
|
|
1409
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
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
|
}
|