@agent-link/agent 0.1.213 → 0.1.215
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 +139 -268
- 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) {
|
|
@@ -519,7 +497,7 @@ function handleServerMessage(msg) {
|
|
|
519
497
|
const projectName = msg.projectName;
|
|
520
498
|
const icmId = msg.icmId;
|
|
521
499
|
const actionItemId = msg.actionItemId;
|
|
522
|
-
const
|
|
500
|
+
const chatWorkDir = existingConv?.workDir || state.workDir;
|
|
523
501
|
const effectiveBrainMode = !!isBrainMode;
|
|
524
502
|
console.log(`[AgentLink] chat: conversationId=${chatConvId}, existingConv.planMode=${existingConv?.planMode}, brainMode=${effectiveBrainMode}`);
|
|
525
503
|
const chatOptions = {
|
|
@@ -549,56 +527,19 @@ function handleServerMessage(msg) {
|
|
|
549
527
|
if (icmId) {
|
|
550
528
|
chatOptions.icmId = String(icmId);
|
|
551
529
|
}
|
|
552
|
-
const chatDir =
|
|
553
|
-
?? existingConv?.workDir
|
|
554
|
-
?? ((recapId || briefingDate || devopsEntityType || projectName || icmId) ? BRAIN_DATA_DIR : null)
|
|
555
|
-
?? state.workDir;
|
|
530
|
+
const chatDir = (recapId || briefingDate || devopsEntityType || projectName || icmId) ? BRAIN_DATA_DIR : (existingConv?.workDir || state.workDir);
|
|
556
531
|
// Track actionItemId for linking to Claude session on session_started
|
|
557
532
|
if (actionItemId && chatConvId) {
|
|
558
533
|
pendingActionItems.set(chatConvId, actionItemId);
|
|
559
534
|
}
|
|
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
|
-
});
|
|
535
|
+
claudeHandleChat(chatConvId, msg.prompt, chatDir, chatOptions, msg.files);
|
|
583
536
|
break;
|
|
584
537
|
}
|
|
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
|
-
});
|
|
538
|
+
case 'cancel_execution':
|
|
539
|
+
claudeCancelExecution(msg.conversationId);
|
|
595
540
|
break;
|
|
596
|
-
}
|
|
597
541
|
case 'list_sessions':
|
|
598
|
-
handleListSessions()
|
|
599
|
-
console.error('[AgentLink] handleListSessions failed:', err);
|
|
600
|
-
send({ type: 'sessions_list', sessions: [], workDir: state.workDir });
|
|
601
|
-
});
|
|
542
|
+
handleListSessions();
|
|
602
543
|
break;
|
|
603
544
|
case 'list_recent_sessions':
|
|
604
545
|
handleListRecentSessions(msg);
|
|
@@ -663,10 +604,6 @@ function handleServerMessage(msg) {
|
|
|
663
604
|
// Backward compat: old web client sends this to reset the single conversation
|
|
664
605
|
abortClaude();
|
|
665
606
|
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
607
|
console.log('[AgentLink] New conversation — session cleared');
|
|
671
608
|
break;
|
|
672
609
|
case 'resume_conversation': {
|
|
@@ -684,148 +621,93 @@ function handleServerMessage(msg) {
|
|
|
684
621
|
// (handles page refresh where web client generates a new UUID)
|
|
685
622
|
rebindConversation(m.claudeSessionId, convId);
|
|
686
623
|
}
|
|
687
|
-
//
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
: [];
|
|
714
|
-
if (history.length > 0)
|
|
715
|
-
resolvedWorkDir = os.homedir();
|
|
624
|
+
// Try current workDir first; fall back to BRAIN_DATA_DIR for recap/briefing chat sessions
|
|
625
|
+
let history = readSessionMessages(state.workDir, m.claudeSessionId);
|
|
626
|
+
if (history.length === 0) {
|
|
627
|
+
const sessionMeta_ = loadSessionMetadata(m.claudeSessionId);
|
|
628
|
+
if (sessionMeta_.recapId || sessionMeta_.briefingDate || sessionMeta_.devopsEntityType || sessionMeta_.projectName || sessionMeta_.icmId) {
|
|
629
|
+
history = readSessionMessages(BRAIN_DATA_DIR, m.claudeSessionId);
|
|
716
630
|
}
|
|
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
|
-
// in the same directory (Claude refuses --resume across cwds).
|
|
743
|
-
currentConv.workDir = resolvedWorkDir;
|
|
631
|
+
}
|
|
632
|
+
// Fallback for BrainCore-spawned action-item sessions: try user home directory.
|
|
633
|
+
// Action-item Claude sessions are spawned by BrainCore with cwd=$HOME and have
|
|
634
|
+
// no AgentLink session-metadata sidecar, so neither lookup above finds them.
|
|
635
|
+
if (history.length === 0) {
|
|
636
|
+
history = readSessionMessages(os.homedir(), m.claudeSessionId);
|
|
637
|
+
}
|
|
638
|
+
console.log(`[AgentLink] → conversation_resumed (${history.length} messages, session ${m.claudeSessionId.slice(0, 8)})`);
|
|
639
|
+
// Include live status so the web client can restore compacting/processing state
|
|
640
|
+
// In multi-session mode, look up by conversationId; in single-session mode, use default
|
|
641
|
+
const currentConv = convId ? getConversation(convId) : getConversation();
|
|
642
|
+
const isSameSession = currentConv?.claudeSessionId === m.claudeSessionId
|
|
643
|
+
|| currentConv?.lastClaudeSessionId === m.claudeSessionId;
|
|
644
|
+
// Restore persisted permission mode (falls back to 'normal' if not found)
|
|
645
|
+
const persistedPermission = getConversationPermission(m.claudeSessionId) || 'normal';
|
|
646
|
+
if (currentConv) {
|
|
647
|
+
const currentMode = currentConv.permissionMode ?? (currentConv.planMode ? 'plan' : 'normal');
|
|
648
|
+
const persistedMode = persistedPermission;
|
|
649
|
+
const modeChanged = currentMode !== persistedMode;
|
|
650
|
+
currentConv.planMode = persistedMode === 'plan';
|
|
651
|
+
currentConv.permissionMode = persistedMode;
|
|
652
|
+
// Only restart when the persisted mode differs from what's running,
|
|
653
|
+
// and never interrupt an active turn.
|
|
654
|
+
if (modeChanged && currentConv.child && !currentConv.turnActive) {
|
|
655
|
+
restartConversation(convId, { planMode: currentConv.planMode, permissionMode: currentConv.permissionMode });
|
|
744
656
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
657
|
+
}
|
|
658
|
+
send({
|
|
659
|
+
type: 'conversation_resumed',
|
|
660
|
+
conversationId: convId,
|
|
661
|
+
claudeSessionId: m.claudeSessionId,
|
|
662
|
+
history,
|
|
663
|
+
isCompacting: isSameSession && (convId ? getIsCompacting(convId) : getIsCompacting()),
|
|
664
|
+
isProcessing: isSameSession && currentConv?.turnActive === true,
|
|
665
|
+
planMode: persistedPermission === 'plan',
|
|
666
|
+
permissionMode: persistedPermission,
|
|
667
|
+
...(typeof m.scrollToMessageIdx === 'number' ? { scrollToMessageIdx: m.scrollToMessageIdx } : {}),
|
|
668
|
+
...(m.searchQuery ? { searchQuery: m.searchQuery } : {}),
|
|
669
|
+
});
|
|
670
|
+
// Re-deliver any pending AskUserQuestion requests so the refreshed web client
|
|
671
|
+
// can display the question card and allow the user to answer.
|
|
672
|
+
if (convId && isSameSession) {
|
|
673
|
+
const pending = getPendingQuestions(convId);
|
|
674
|
+
for (const pq of pending) {
|
|
675
|
+
send({
|
|
676
|
+
type: 'ask_user_question',
|
|
677
|
+
conversationId: convId,
|
|
678
|
+
requestId: pq.requestId,
|
|
679
|
+
questions: pq.questions,
|
|
680
|
+
});
|
|
761
681
|
}
|
|
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
|
-
}
|
|
682
|
+
// Re-deliver pending tool permission requests (same pattern as ask_user_question)
|
|
683
|
+
const pendingPerms = getPendingToolPermissions(convId);
|
|
684
|
+
for (const pp of pendingPerms) {
|
|
685
|
+
send({
|
|
686
|
+
type: 'tool_permission_request',
|
|
687
|
+
conversationId: convId,
|
|
688
|
+
requestId: pp.requestId,
|
|
689
|
+
toolName: pp.toolName,
|
|
690
|
+
displayName: pp.displayName,
|
|
691
|
+
input: pp.input,
|
|
692
|
+
decisionReason: pp.decisionReason,
|
|
693
|
+
});
|
|
799
694
|
}
|
|
800
|
-
}
|
|
801
|
-
console.error('[AgentLink] resume_conversation failed:', err);
|
|
802
|
-
send({ type: 'error', message: 'Failed to resume conversation.' });
|
|
803
|
-
});
|
|
695
|
+
}
|
|
804
696
|
break;
|
|
805
697
|
}
|
|
806
698
|
case 'ask_user_answer': {
|
|
807
699
|
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);
|
|
700
|
+
handleUserAnswer(m.requestId, m.answers);
|
|
813
701
|
break;
|
|
814
702
|
}
|
|
815
703
|
case 'delete_session': {
|
|
816
704
|
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
|
-
});
|
|
705
|
+
handleDeleteSession(m.sessionId);
|
|
821
706
|
break;
|
|
822
707
|
}
|
|
823
708
|
case 'rename_session': {
|
|
824
709
|
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
|
-
});
|
|
710
|
+
handleRenameSession(m.sessionId, m.newTitle);
|
|
829
711
|
break;
|
|
830
712
|
}
|
|
831
713
|
case 'query_active_conversations': {
|
|
@@ -836,7 +718,7 @@ function handleServerMessage(msg) {
|
|
|
836
718
|
conversationId: convId,
|
|
837
719
|
claudeSessionId: conv.claudeSessionId,
|
|
838
720
|
isProcessing: true,
|
|
839
|
-
isCompacting:
|
|
721
|
+
isCompacting: getIsCompacting(convId),
|
|
840
722
|
});
|
|
841
723
|
}
|
|
842
724
|
else if (conv.claudeSessionId) {
|
|
@@ -950,11 +832,11 @@ function handleServerMessage(msg) {
|
|
|
950
832
|
break;
|
|
951
833
|
}
|
|
952
834
|
console.log(`[AgentLink] set_model: model=${model}, conversationId=${conversationId}`);
|
|
953
|
-
|
|
835
|
+
setModel(conversationId, model);
|
|
954
836
|
// Kill current Claude process so next message spawns with new --model flag
|
|
955
837
|
const conv = getConversation(conversationId);
|
|
956
838
|
if (conv) {
|
|
957
|
-
|
|
839
|
+
restartConversation(conversationId);
|
|
958
840
|
}
|
|
959
841
|
send({ type: 'model_changed', model, conversationId });
|
|
960
842
|
break;
|
|
@@ -962,10 +844,17 @@ function handleServerMessage(msg) {
|
|
|
962
844
|
case 'set_plan_mode': {
|
|
963
845
|
const { enabled, conversationId } = msg;
|
|
964
846
|
console.log(`[AgentLink] set_plan_mode: enabled=${enabled}, conversationId=${conversationId}`);
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
847
|
+
const conv = getConversation(conversationId);
|
|
848
|
+
if (conv) {
|
|
849
|
+
// Restart with new mode
|
|
850
|
+
const result = restartConversation(conversationId, { planMode: enabled });
|
|
851
|
+
if (result.wasTurnActive) {
|
|
852
|
+
send({ type: 'execution_cancelled', conversationId });
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
// No conversation yet — create placeholder
|
|
857
|
+
createPlaceholderConversation(conversationId, { planMode: enabled });
|
|
969
858
|
}
|
|
970
859
|
send({ type: 'plan_mode_changed', enabled, conversationId });
|
|
971
860
|
break;
|
|
@@ -973,10 +862,22 @@ function handleServerMessage(msg) {
|
|
|
973
862
|
case 'set_permission_mode': {
|
|
974
863
|
const { mode, conversationId } = msg;
|
|
975
864
|
console.log(`[AgentLink] set_permission_mode: mode=${mode}, conversationId=${conversationId}`);
|
|
976
|
-
const
|
|
977
|
-
const
|
|
978
|
-
if (
|
|
979
|
-
|
|
865
|
+
const planMode = mode === 'plan';
|
|
866
|
+
const conv = getConversation(conversationId);
|
|
867
|
+
if (conv) {
|
|
868
|
+
const result = restartConversation(conversationId, {
|
|
869
|
+
planMode,
|
|
870
|
+
permissionMode: mode,
|
|
871
|
+
});
|
|
872
|
+
if (result.wasTurnActive) {
|
|
873
|
+
send({ type: 'execution_cancelled', conversationId });
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
createPlaceholderConversation(conversationId, {
|
|
878
|
+
planMode,
|
|
879
|
+
permissionMode: mode,
|
|
880
|
+
});
|
|
980
881
|
}
|
|
981
882
|
send({ type: 'permission_mode_changed', mode, conversationId });
|
|
982
883
|
// Persist permission mode for this conversation's session
|
|
@@ -993,20 +894,23 @@ function handleServerMessage(msg) {
|
|
|
993
894
|
case 'tool_permission_response': {
|
|
994
895
|
const { requestId, behavior } = msg;
|
|
995
896
|
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);
|
|
897
|
+
handleToolPermissionResponse(requestId, behavior);
|
|
1001
898
|
break;
|
|
1002
899
|
}
|
|
1003
900
|
case 'set_brain_mode': {
|
|
1004
901
|
const { enabled, conversationId } = msg;
|
|
1005
902
|
console.log(`[AgentLink] set_brain_mode: enabled=${enabled}, conversationId=${conversationId}`);
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
903
|
+
const conv = getConversation(conversationId);
|
|
904
|
+
if (conv) {
|
|
905
|
+
// Restart with new brain mode
|
|
906
|
+
const result = restartConversation(conversationId, { brainMode: enabled });
|
|
907
|
+
if (result.wasTurnActive) {
|
|
908
|
+
send({ type: 'execution_cancelled', conversationId });
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
else {
|
|
912
|
+
// No conversation yet — create placeholder
|
|
913
|
+
createPlaceholderConversation(conversationId, { brainMode: enabled });
|
|
1010
914
|
}
|
|
1011
915
|
send({ type: 'brain_mode_changed', enabled, conversationId });
|
|
1012
916
|
break;
|
|
@@ -1404,38 +1308,18 @@ async function handleBrainChatDeleteSession(msg) {
|
|
|
1404
1308
|
send({ type: 'brainchat_error', message: String(err) });
|
|
1405
1309
|
}
|
|
1406
1310
|
}
|
|
1407
|
-
|
|
1311
|
+
function handleListSessions() {
|
|
1408
1312
|
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
|
-
}));
|
|
1313
|
+
const sessions = listSessions(state.workDir);
|
|
1423
1314
|
const metaMap = loadAllSessionMetadata();
|
|
1424
1315
|
const enriched = sessions.map(s => ({
|
|
1425
1316
|
...s,
|
|
1426
1317
|
...metaMap.get(s.sessionId),
|
|
1427
1318
|
}));
|
|
1428
1319
|
// Always merge recap/briefing/devops chat sessions from BrainData directory — they live under a
|
|
1429
|
-
// different Claude project folder, so
|
|
1320
|
+
// different Claude project folder, so listSessions(state.workDir) misses them.
|
|
1430
1321
|
// 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
|
-
}));
|
|
1322
|
+
const brainSessions = listSessions(BRAIN_DATA_DIR);
|
|
1439
1323
|
const existingIds = new Set(enriched.map(s => s.sessionId));
|
|
1440
1324
|
for (const bs of brainSessions) {
|
|
1441
1325
|
if (existingIds.has(bs.sessionId))
|
|
@@ -1520,22 +1404,18 @@ function handleCancelSearch(msg) {
|
|
|
1520
1404
|
searchControllers.delete(msg.searchId);
|
|
1521
1405
|
}
|
|
1522
1406
|
}
|
|
1523
|
-
|
|
1407
|
+
function handleDeleteSession(sessionId) {
|
|
1524
1408
|
// Evict any idle conversation holding this session; block if busy
|
|
1525
1409
|
if (evictByClaudeSessionId(sessionId)) {
|
|
1526
1410
|
send({ type: 'error', message: 'Cannot delete a session while it is processing.' });
|
|
1527
1411
|
return;
|
|
1528
1412
|
}
|
|
1529
|
-
if (!runtime) {
|
|
1530
|
-
send({ type: 'error', message: 'Session not found or could not be deleted.' });
|
|
1531
|
-
return;
|
|
1532
|
-
}
|
|
1533
1413
|
// Try current workDir first; if not found, check if it's a recap/briefing/devops/project session in BrainData
|
|
1534
|
-
let deleted =
|
|
1414
|
+
let deleted = deleteSession(state.workDir, sessionId);
|
|
1535
1415
|
if (!deleted) {
|
|
1536
1416
|
const meta = loadSessionMetadata(sessionId);
|
|
1537
1417
|
if (meta.recapId || meta.briefingDate || meta.devopsEntityType || meta.projectName || meta.icmId) {
|
|
1538
|
-
deleted =
|
|
1418
|
+
deleted = deleteSession(BRAIN_DATA_DIR, sessionId);
|
|
1539
1419
|
}
|
|
1540
1420
|
}
|
|
1541
1421
|
if (deleted) {
|
|
@@ -1546,17 +1426,13 @@ async function handleDeleteSession(sessionId) {
|
|
|
1546
1426
|
send({ type: 'error', message: 'Session not found or could not be deleted.' });
|
|
1547
1427
|
}
|
|
1548
1428
|
}
|
|
1549
|
-
|
|
1550
|
-
if (!runtime) {
|
|
1551
|
-
send({ type: 'error', message: 'Session not found or could not be renamed.' });
|
|
1552
|
-
return;
|
|
1553
|
-
}
|
|
1429
|
+
function handleRenameSession(sessionId, newTitle) {
|
|
1554
1430
|
// Try current workDir first; if not found, check if it's a recap/briefing/devops/project session in BrainData
|
|
1555
|
-
let renamed =
|
|
1431
|
+
let renamed = renameSession(state.workDir, sessionId, newTitle);
|
|
1556
1432
|
if (!renamed) {
|
|
1557
1433
|
const meta = loadSessionMetadata(sessionId);
|
|
1558
1434
|
if (meta.recapId || meta.briefingDate || meta.devopsEntityType || meta.projectName || meta.icmId) {
|
|
1559
|
-
renamed =
|
|
1435
|
+
renamed = renameSession(BRAIN_DATA_DIR, sessionId, newTitle);
|
|
1560
1436
|
}
|
|
1561
1437
|
}
|
|
1562
1438
|
if (renamed) {
|
|
@@ -1566,20 +1442,15 @@ async function handleRenameSession(sessionId, newTitle) {
|
|
|
1566
1442
|
// Session file may not exist on disk yet (e.g. IcM/project chat still streaming first response).
|
|
1567
1443
|
// Retry once after a short delay; if it still fails, send an error so the UI can recover.
|
|
1568
1444
|
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);
|
|
1445
|
+
const retried = renameSession(state.workDir, sessionId, newTitle)
|
|
1446
|
+
|| renameSession(BRAIN_DATA_DIR, sessionId, newTitle);
|
|
1447
|
+
if (retried) {
|
|
1448
|
+
send({ type: 'session_renamed', sessionId, newTitle });
|
|
1449
|
+
}
|
|
1450
|
+
else {
|
|
1451
|
+
console.warn(`[AgentLink] rename_session: session ${sessionId.slice(0, 8)} not found after retry`);
|
|
1581
1452
|
send({ type: 'error', message: 'Session not found or could not be renamed.' });
|
|
1582
|
-
}
|
|
1453
|
+
}
|
|
1583
1454
|
}, 1500);
|
|
1584
1455
|
}
|
|
1585
1456
|
}
|