@agent-link/agent 0.1.204 → 0.1.206
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 +90 -0
- package/dist/backends/claude.js +356 -0
- package/dist/backends/claude.js.map +1 -0
- package/dist/backends/index.d.ts +27 -0
- package/dist/backends/index.js +52 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/backends/types.d.ts +379 -0
- package/dist/backends/types.js +120 -0
- package/dist/backends/types.js.map +1 -0
- package/dist/claude.d.ts +22 -2
- package/dist/claude.js +54 -6
- package/dist/claude.js.map +1 -1
- package/dist/cli.js +62 -2
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/connection.d.ts +4 -1
- package/dist/connection.js +274 -134
- package/dist/connection.js.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/recap.d.ts +1 -0
- package/dist/recap.js.map +1 -1
- package/dist/runtime.d.ts +176 -0
- package/dist/runtime.js +286 -0
- package/dist/runtime.js.map +1 -0
- 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,
|
|
23
|
-
import {
|
|
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';
|
|
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';
|
|
@@ -46,6 +46,15 @@ const state = {
|
|
|
46
46
|
config: null,
|
|
47
47
|
entraToken: null,
|
|
48
48
|
};
|
|
49
|
+
let unsubscribeSend = null;
|
|
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
|
+
}
|
|
49
58
|
let heartbeatInterval = null;
|
|
50
59
|
let heartbeatTimeout = null;
|
|
51
60
|
// Wire Entra timer module to this connection's state and send fn.
|
|
@@ -69,6 +78,14 @@ export function connect(config) {
|
|
|
69
78
|
state.workDir = config.dir;
|
|
70
79
|
state.config = config;
|
|
71
80
|
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();
|
|
72
89
|
// Initialize E2E encryption key
|
|
73
90
|
const ignoreConfig = !!process.env.AGENTLINK_NO_STATE;
|
|
74
91
|
state.e2eKey = getOrCreateE2EKey(ignoreConfig);
|
|
@@ -78,10 +95,24 @@ export function connect(config) {
|
|
|
78
95
|
state.sessionId = prev.sessionId;
|
|
79
96
|
console.log(`[AgentLink] Restoring session: ${prev.sessionId}`);
|
|
80
97
|
}
|
|
81
|
-
// Wire up the Claude module to send messages through our WebSocket
|
|
82
|
-
|
|
98
|
+
// Wire up the Claude module to send messages through our WebSocket.
|
|
99
|
+
// Use additive subscribers so the ClaudeBackend adapter (PR3+) can coexist;
|
|
100
|
+
// capture unsubscribes so disconnect() can detach without leaks.
|
|
101
|
+
if (unsubscribeSend) {
|
|
102
|
+
try {
|
|
103
|
+
unsubscribeSend();
|
|
104
|
+
}
|
|
105
|
+
catch { /* ignore */ }
|
|
106
|
+
}
|
|
107
|
+
unsubscribeSend = addSendFn(send);
|
|
83
108
|
// Wire up action item session linkage
|
|
84
|
-
|
|
109
|
+
if (unsubscribeSessionStarted) {
|
|
110
|
+
try {
|
|
111
|
+
unsubscribeSessionStarted();
|
|
112
|
+
}
|
|
113
|
+
catch { /* ignore */ }
|
|
114
|
+
}
|
|
115
|
+
unsubscribeSessionStarted = addOnSessionStarted(checkPendingActionItem);
|
|
85
116
|
// Initialize the tunnel handler for port proxy
|
|
86
117
|
tunnelHandler = createTunnelHandler(send);
|
|
87
118
|
// Initialize the terminal manager for web terminal
|
|
@@ -339,6 +370,20 @@ export function disconnect() {
|
|
|
339
370
|
stopEntraRetryTimer();
|
|
340
371
|
cancelEntraRefresh();
|
|
341
372
|
abortAllClaude();
|
|
373
|
+
if (unsubscribeSend) {
|
|
374
|
+
try {
|
|
375
|
+
unsubscribeSend();
|
|
376
|
+
}
|
|
377
|
+
catch { /* ignore */ }
|
|
378
|
+
unsubscribeSend = null;
|
|
379
|
+
}
|
|
380
|
+
if (unsubscribeSessionStarted) {
|
|
381
|
+
try {
|
|
382
|
+
unsubscribeSessionStarted();
|
|
383
|
+
}
|
|
384
|
+
catch { /* ignore */ }
|
|
385
|
+
unsubscribeSessionStarted = null;
|
|
386
|
+
}
|
|
342
387
|
if (tunnelHandler)
|
|
343
388
|
tunnelHandler.cleanup();
|
|
344
389
|
if (terminalManager)
|
|
@@ -347,6 +392,13 @@ export function disconnect() {
|
|
|
347
392
|
state.ws.close();
|
|
348
393
|
state.ws = null;
|
|
349
394
|
}
|
|
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();
|
|
350
402
|
}
|
|
351
403
|
let sendQueue = Promise.resolve();
|
|
352
404
|
export function send(msg) {
|
|
@@ -405,6 +457,9 @@ function buildWsUrl(config) {
|
|
|
405
457
|
if (config.entra) {
|
|
406
458
|
params.set('entra', '1');
|
|
407
459
|
}
|
|
460
|
+
// Forward selected backend so the server can route accordingly. Default
|
|
461
|
+
// 'claude' makes this a no-op for existing servers that ignore the param.
|
|
462
|
+
params.set('backendType', config.agent ?? 'claude');
|
|
408
463
|
return `${base}/?${params}`;
|
|
409
464
|
}
|
|
410
465
|
function scheduleReconnect(config) {
|
|
@@ -499,14 +554,48 @@ function handleServerMessage(msg) {
|
|
|
499
554
|
if (actionItemId && chatConvId) {
|
|
500
555
|
pendingActionItems.set(chatConvId, actionItemId);
|
|
501
556
|
}
|
|
502
|
-
|
|
557
|
+
// PR4-B: route through AgentRuntime. ClaudeBackend forwards `metadata`
|
|
558
|
+
// (brainMode/recapId/briefingDate/devops*/projectName/icmId) as
|
|
559
|
+
// HandleChatOptions to claude.handleChat. .catch() so any rejection
|
|
560
|
+
// from ensureSession/backend.startTurn is logged rather than becoming
|
|
561
|
+
// an unhandled rejection (Codex review #1, high). handleServerMessage
|
|
562
|
+
// is sync, so we can't await; the caller in ws.on('message') has its
|
|
563
|
+
// own try/catch but it would miss async rejections from this call.
|
|
564
|
+
// Use 'default' as the conv key when missing — matches claude.ts's
|
|
565
|
+
// DEFAULT_CONVERSATION_ID so the runtime map and backend's internal
|
|
566
|
+
// map agree. Empty string would create a divergent mapping (Copilot
|
|
567
|
+
// round 1).
|
|
568
|
+
const startConvId = chatConvId ?? 'default';
|
|
569
|
+
if (!runtime) {
|
|
570
|
+
console.warn('[AgentLink] chat received after runtime shutdown — ignoring');
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
runtime.startTurn(startConvId, {
|
|
574
|
+
text: msg.prompt,
|
|
575
|
+
files: msg.files,
|
|
576
|
+
metadata: chatOptions,
|
|
577
|
+
}, { workDir: chatDir, resumeSessionId: chatOptions.resumeSessionId }).catch((err) => {
|
|
578
|
+
console.error('[AgentLink] runtime.startTurn failed:', err.message);
|
|
579
|
+
});
|
|
503
580
|
break;
|
|
504
581
|
}
|
|
505
|
-
case 'cancel_execution':
|
|
506
|
-
|
|
582
|
+
case 'cancel_execution': {
|
|
583
|
+
const cancelConvId = msg.conversationId;
|
|
584
|
+
// 'default' default matches claude.ts (Copilot round 1).
|
|
585
|
+
if (!runtime) {
|
|
586
|
+
console.warn('[AgentLink] cancel received after runtime shutdown — ignoring');
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
runtime.interruptTurn(cancelConvId ?? 'default').catch((err) => {
|
|
590
|
+
console.error('[AgentLink] runtime.interruptTurn failed:', err.message);
|
|
591
|
+
});
|
|
507
592
|
break;
|
|
593
|
+
}
|
|
508
594
|
case 'list_sessions':
|
|
509
|
-
handleListSessions()
|
|
595
|
+
handleListSessions().catch((err) => {
|
|
596
|
+
console.error('[AgentLink] handleListSessions failed:', err);
|
|
597
|
+
send({ type: 'sessions_list', sessions: [], workDir: state.workDir });
|
|
598
|
+
});
|
|
510
599
|
break;
|
|
511
600
|
case 'list_recent_sessions':
|
|
512
601
|
handleListRecentSessions(msg);
|
|
@@ -571,6 +660,10 @@ function handleServerMessage(msg) {
|
|
|
571
660
|
// Backward compat: old web client sends this to reset the single conversation
|
|
572
661
|
abortClaude();
|
|
573
662
|
clearSessionId('default');
|
|
663
|
+
// Evict the runtime's cached ref so the next chat re-ensures (otherwise
|
|
664
|
+
// ClaudeBackend would compute resumeSessionId from the stale ref and
|
|
665
|
+
// resume the just-cleared session). Copilot round 3 fix.
|
|
666
|
+
runtime?.evictConversation('default');
|
|
574
667
|
console.log('[AgentLink] New conversation — session cleared');
|
|
575
668
|
break;
|
|
576
669
|
case 'resume_conversation': {
|
|
@@ -588,87 +681,123 @@ function handleServerMessage(msg) {
|
|
|
588
681
|
// (handles page refresh where web client generates a new UUID)
|
|
589
682
|
rebindConversation(m.claudeSessionId, convId);
|
|
590
683
|
}
|
|
591
|
-
//
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
684
|
+
// History reads went async in PR4-D (runtime.readHistory is async).
|
|
685
|
+
// Wrap the rest of the handler in an async IIFE so we can await without
|
|
686
|
+
// making the entire switch dispatch async. .catch() ensures backend
|
|
687
|
+
// rejections don't become unhandled promise rejections.
|
|
688
|
+
(async () => {
|
|
689
|
+
// Try current workDir first; fall back to BRAIN_DATA_DIR for recap/briefing chat sessions
|
|
690
|
+
let history = runtime
|
|
691
|
+
? await runtime.readHistory(state.workDir, m.claudeSessionId)
|
|
692
|
+
: [];
|
|
693
|
+
if (history.length === 0) {
|
|
694
|
+
const sessionMeta_ = loadSessionMetadata(m.claudeSessionId);
|
|
695
|
+
if (sessionMeta_.recapId || sessionMeta_.briefingDate || sessionMeta_.devopsEntityType || sessionMeta_.projectName || sessionMeta_.icmId) {
|
|
696
|
+
history = runtime
|
|
697
|
+
? await runtime.readHistory(BRAIN_DATA_DIR, m.claudeSessionId)
|
|
698
|
+
: [];
|
|
699
|
+
}
|
|
597
700
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
// Restore persisted permission mode (falls back to 'normal' if not found)
|
|
606
|
-
const persistedPermission = getConversationPermission(m.claudeSessionId) || 'normal';
|
|
607
|
-
if (currentConv) {
|
|
608
|
-
const currentMode = currentConv.permissionMode ?? (currentConv.planMode ? 'plan' : 'normal');
|
|
609
|
-
const persistedMode = persistedPermission;
|
|
610
|
-
const modeChanged = currentMode !== persistedMode;
|
|
611
|
-
currentConv.planMode = persistedMode === 'plan';
|
|
612
|
-
currentConv.permissionMode = persistedMode;
|
|
613
|
-
// Only restart when the persisted mode differs from what's running,
|
|
614
|
-
// and never interrupt an active turn.
|
|
615
|
-
if (modeChanged && currentConv.child && !currentConv.turnActive) {
|
|
616
|
-
restartConversation(convId, { planMode: currentConv.planMode, permissionMode: currentConv.permissionMode });
|
|
701
|
+
// Fallback for BrainCore-spawned action-item sessions: try user home directory.
|
|
702
|
+
// Action-item Claude sessions are spawned by BrainCore with cwd=$HOME and have
|
|
703
|
+
// no AgentLink session-metadata sidecar, so neither lookup above finds them.
|
|
704
|
+
if (history.length === 0) {
|
|
705
|
+
history = runtime
|
|
706
|
+
? await runtime.readHistory(os.homedir(), m.claudeSessionId)
|
|
707
|
+
: [];
|
|
617
708
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
claudeSessionId
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
709
|
+
console.log(`[AgentLink] → conversation_resumed (${history.length} messages, session ${m.claudeSessionId.slice(0, 8)})`);
|
|
710
|
+
// Include live status so the web client can restore compacting/processing state
|
|
711
|
+
// In multi-session mode, look up by conversationId; in single-session mode, use default
|
|
712
|
+
const currentConv = convId ? getConversation(convId) : getConversation();
|
|
713
|
+
const isSameSession = currentConv?.claudeSessionId === m.claudeSessionId
|
|
714
|
+
|| currentConv?.lastClaudeSessionId === m.claudeSessionId;
|
|
715
|
+
// Restore persisted permission mode (falls back to 'normal' if not found)
|
|
716
|
+
const persistedPermission = getConversationPermission(m.claudeSessionId) || 'normal';
|
|
717
|
+
if (currentConv) {
|
|
718
|
+
const currentMode = currentConv.permissionMode ?? (currentConv.planMode ? 'plan' : 'normal');
|
|
719
|
+
const persistedMode = persistedPermission;
|
|
720
|
+
const modeChanged = currentMode !== persistedMode;
|
|
721
|
+
currentConv.planMode = persistedMode === 'plan';
|
|
722
|
+
currentConv.permissionMode = persistedMode;
|
|
723
|
+
// Only restart when the persisted mode differs from what's running,
|
|
724
|
+
// and never interrupt an active turn.
|
|
725
|
+
if (modeChanged && currentConv.child && !currentConv.turnActive) {
|
|
726
|
+
const cid = convId ?? 'default';
|
|
727
|
+
runtime?.restartConversation(cid, { planMode: currentConv.planMode, permissionMode: currentConv.permissionMode });
|
|
728
|
+
// Re-seed runtime mapping with the resumed session id so the next
|
|
729
|
+
// chat resumes m.claudeSessionId rather than starting fresh after
|
|
730
|
+
// restartConversation evicts the cache. (Codex review round 1.)
|
|
731
|
+
runtime?.ensureConversation(cid, { workDir: state.workDir, resumeSessionId: m.claudeSessionId }).catch(() => { });
|
|
732
|
+
}
|
|
642
733
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
734
|
+
send({
|
|
735
|
+
type: 'conversation_resumed',
|
|
736
|
+
conversationId: convId,
|
|
737
|
+
claudeSessionId: m.claudeSessionId,
|
|
738
|
+
history,
|
|
739
|
+
isCompacting: isSameSession && (runtime?.isCompacting(convId) ?? false),
|
|
740
|
+
isProcessing: isSameSession && currentConv?.turnActive === true,
|
|
741
|
+
planMode: persistedPermission === 'plan',
|
|
742
|
+
permissionMode: persistedPermission,
|
|
743
|
+
...(typeof m.scrollToMessageIdx === 'number' ? { scrollToMessageIdx: m.scrollToMessageIdx } : {}),
|
|
744
|
+
...(m.searchQuery ? { searchQuery: m.searchQuery } : {}),
|
|
745
|
+
});
|
|
746
|
+
// Re-deliver any pending AskUserQuestion requests so the refreshed web client
|
|
747
|
+
// can display the question card and allow the user to answer.
|
|
748
|
+
if (convId && isSameSession) {
|
|
749
|
+
const pending = getPendingQuestions(convId);
|
|
750
|
+
for (const pq of pending) {
|
|
751
|
+
send({
|
|
752
|
+
type: 'ask_user_question',
|
|
753
|
+
conversationId: convId,
|
|
754
|
+
requestId: pq.requestId,
|
|
755
|
+
questions: pq.questions,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
// Re-deliver pending tool permission requests (same pattern as ask_user_question)
|
|
759
|
+
const pendingPerms = getPendingToolPermissions(convId);
|
|
760
|
+
for (const pp of pendingPerms) {
|
|
761
|
+
send({
|
|
762
|
+
type: 'tool_permission_request',
|
|
763
|
+
conversationId: convId,
|
|
764
|
+
requestId: pp.requestId,
|
|
765
|
+
toolName: pp.toolName,
|
|
766
|
+
displayName: pp.displayName,
|
|
767
|
+
input: pp.input,
|
|
768
|
+
decisionReason: pp.decisionReason,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
655
771
|
}
|
|
656
|
-
}
|
|
772
|
+
})().catch((err) => {
|
|
773
|
+
console.error('[AgentLink] resume_conversation failed:', err);
|
|
774
|
+
send({ type: 'error', message: 'Failed to resume conversation.' });
|
|
775
|
+
});
|
|
657
776
|
break;
|
|
658
777
|
}
|
|
659
778
|
case 'ask_user_answer': {
|
|
660
779
|
const m = msg;
|
|
661
|
-
|
|
780
|
+
if (!runtime) {
|
|
781
|
+
console.warn('[AgentLink] ask_user_answer received after runtime shutdown — ignoring');
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
runtime.answerUserInput(m.requestId, m.answers);
|
|
662
785
|
break;
|
|
663
786
|
}
|
|
664
787
|
case 'delete_session': {
|
|
665
788
|
const m = msg;
|
|
666
|
-
handleDeleteSession(m.sessionId)
|
|
789
|
+
handleDeleteSession(m.sessionId).catch((err) => {
|
|
790
|
+
console.error('[AgentLink] handleDeleteSession failed:', err);
|
|
791
|
+
send({ type: 'error', message: 'Session not found or could not be deleted.' });
|
|
792
|
+
});
|
|
667
793
|
break;
|
|
668
794
|
}
|
|
669
795
|
case 'rename_session': {
|
|
670
796
|
const m = msg;
|
|
671
|
-
handleRenameSession(m.sessionId, m.newTitle)
|
|
797
|
+
handleRenameSession(m.sessionId, m.newTitle).catch((err) => {
|
|
798
|
+
console.error('[AgentLink] handleRenameSession failed:', err);
|
|
799
|
+
send({ type: 'error', message: 'Session not found or could not be renamed.' });
|
|
800
|
+
});
|
|
672
801
|
break;
|
|
673
802
|
}
|
|
674
803
|
case 'query_active_conversations': {
|
|
@@ -679,7 +808,7 @@ function handleServerMessage(msg) {
|
|
|
679
808
|
conversationId: convId,
|
|
680
809
|
claudeSessionId: conv.claudeSessionId,
|
|
681
810
|
isProcessing: true,
|
|
682
|
-
isCompacting:
|
|
811
|
+
isCompacting: runtime?.isCompacting(convId) ?? false,
|
|
683
812
|
});
|
|
684
813
|
}
|
|
685
814
|
else if (conv.claudeSessionId) {
|
|
@@ -793,11 +922,11 @@ function handleServerMessage(msg) {
|
|
|
793
922
|
break;
|
|
794
923
|
}
|
|
795
924
|
console.log(`[AgentLink] set_model: model=${model}, conversationId=${conversationId}`);
|
|
796
|
-
setModel(conversationId, model);
|
|
925
|
+
runtime?.setModel(conversationId ?? null, model);
|
|
797
926
|
// Kill current Claude process so next message spawns with new --model flag
|
|
798
927
|
const conv = getConversation(conversationId);
|
|
799
928
|
if (conv) {
|
|
800
|
-
restartConversation(conversationId);
|
|
929
|
+
runtime?.restartConversation(conversationId ?? 'default');
|
|
801
930
|
}
|
|
802
931
|
send({ type: 'model_changed', model, conversationId });
|
|
803
932
|
break;
|
|
@@ -805,17 +934,10 @@ function handleServerMessage(msg) {
|
|
|
805
934
|
case 'set_plan_mode': {
|
|
806
935
|
const { enabled, conversationId } = msg;
|
|
807
936
|
console.log(`[AgentLink] set_plan_mode: enabled=${enabled}, conversationId=${conversationId}`);
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
if (result.wasTurnActive) {
|
|
813
|
-
send({ type: 'execution_cancelled', conversationId });
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
else {
|
|
817
|
-
// No conversation yet — create placeholder
|
|
818
|
-
createPlaceholderConversation(conversationId, { planMode: enabled });
|
|
937
|
+
const convId = conversationId ?? 'default';
|
|
938
|
+
const result = runtime?.setPlanMode(convId, enabled, state.workDir) ?? null;
|
|
939
|
+
if (result?.wasTurnActive) {
|
|
940
|
+
send({ type: 'execution_cancelled', conversationId });
|
|
819
941
|
}
|
|
820
942
|
send({ type: 'plan_mode_changed', enabled, conversationId });
|
|
821
943
|
break;
|
|
@@ -823,22 +945,10 @@ function handleServerMessage(msg) {
|
|
|
823
945
|
case 'set_permission_mode': {
|
|
824
946
|
const { mode, conversationId } = msg;
|
|
825
947
|
console.log(`[AgentLink] set_permission_mode: mode=${mode}, conversationId=${conversationId}`);
|
|
826
|
-
const
|
|
827
|
-
const
|
|
828
|
-
if (
|
|
829
|
-
|
|
830
|
-
planMode,
|
|
831
|
-
permissionMode: mode,
|
|
832
|
-
});
|
|
833
|
-
if (result.wasTurnActive) {
|
|
834
|
-
send({ type: 'execution_cancelled', conversationId });
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
else {
|
|
838
|
-
createPlaceholderConversation(conversationId, {
|
|
839
|
-
planMode,
|
|
840
|
-
permissionMode: mode,
|
|
841
|
-
});
|
|
948
|
+
const convId = conversationId ?? 'default';
|
|
949
|
+
const result = runtime?.setPermissionMode(convId, mode, state.workDir) ?? null;
|
|
950
|
+
if (result?.wasTurnActive) {
|
|
951
|
+
send({ type: 'execution_cancelled', conversationId });
|
|
842
952
|
}
|
|
843
953
|
send({ type: 'permission_mode_changed', mode, conversationId });
|
|
844
954
|
// Persist permission mode for this conversation's session
|
|
@@ -855,23 +965,20 @@ function handleServerMessage(msg) {
|
|
|
855
965
|
case 'tool_permission_response': {
|
|
856
966
|
const { requestId, behavior } = msg;
|
|
857
967
|
console.log(`[AgentLink] tool_permission_response: ${behavior} (${requestId})`);
|
|
858
|
-
|
|
968
|
+
if (!runtime) {
|
|
969
|
+
console.warn('[AgentLink] tool_permission_response received after runtime shutdown — ignoring');
|
|
970
|
+
break;
|
|
971
|
+
}
|
|
972
|
+
runtime.answerApproval(requestId, behavior);
|
|
859
973
|
break;
|
|
860
974
|
}
|
|
861
975
|
case 'set_brain_mode': {
|
|
862
976
|
const { enabled, conversationId } = msg;
|
|
863
977
|
console.log(`[AgentLink] set_brain_mode: enabled=${enabled}, conversationId=${conversationId}`);
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
if (result.wasTurnActive) {
|
|
869
|
-
send({ type: 'execution_cancelled', conversationId });
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
else {
|
|
873
|
-
// No conversation yet — create placeholder
|
|
874
|
-
createPlaceholderConversation(conversationId, { brainMode: enabled });
|
|
978
|
+
const convId = conversationId ?? 'default';
|
|
979
|
+
const result = runtime?.setBrainMode(convId, enabled, state.workDir) ?? null;
|
|
980
|
+
if (result?.wasTurnActive) {
|
|
981
|
+
send({ type: 'execution_cancelled', conversationId });
|
|
875
982
|
}
|
|
876
983
|
send({ type: 'brain_mode_changed', enabled, conversationId });
|
|
877
984
|
break;
|
|
@@ -1269,18 +1376,38 @@ async function handleBrainChatDeleteSession(msg) {
|
|
|
1269
1376
|
send({ type: 'brainchat_error', message: String(err) });
|
|
1270
1377
|
}
|
|
1271
1378
|
}
|
|
1272
|
-
function handleListSessions() {
|
|
1379
|
+
async function handleListSessions() {
|
|
1273
1380
|
try {
|
|
1274
|
-
|
|
1381
|
+
if (!runtime) {
|
|
1382
|
+
send({ type: 'sessions_list', sessions: [], workDir: state.workDir });
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
// PR4-D: runtime.listHistory returns BackendSessionInfo rows; map to the
|
|
1386
|
+
// flat wire shape connection.ts has always sent ({sessionId, title, ...}).
|
|
1387
|
+
const rows = await runtime.listHistory(state.workDir);
|
|
1388
|
+
const sessions = rows.map(r => ({
|
|
1389
|
+
sessionId: r.session.backendSessionId,
|
|
1390
|
+
title: r.title,
|
|
1391
|
+
customTitle: r.customTitle,
|
|
1392
|
+
preview: r.preview,
|
|
1393
|
+
lastModified: r.lastModified,
|
|
1394
|
+
}));
|
|
1275
1395
|
const metaMap = loadAllSessionMetadata();
|
|
1276
1396
|
const enriched = sessions.map(s => ({
|
|
1277
1397
|
...s,
|
|
1278
1398
|
...metaMap.get(s.sessionId),
|
|
1279
1399
|
}));
|
|
1280
1400
|
// Always merge recap/briefing/devops chat sessions from BrainData directory — they live under a
|
|
1281
|
-
// different Claude project folder, so
|
|
1401
|
+
// different Claude project folder, so listHistory(state.workDir) misses them.
|
|
1282
1402
|
// These sessions should be visible regardless of the current workDir.
|
|
1283
|
-
const
|
|
1403
|
+
const brainRows = await runtime.listHistory(BRAIN_DATA_DIR);
|
|
1404
|
+
const brainSessions = brainRows.map(r => ({
|
|
1405
|
+
sessionId: r.session.backendSessionId,
|
|
1406
|
+
title: r.title,
|
|
1407
|
+
customTitle: r.customTitle,
|
|
1408
|
+
preview: r.preview,
|
|
1409
|
+
lastModified: r.lastModified,
|
|
1410
|
+
}));
|
|
1284
1411
|
const existingIds = new Set(enriched.map(s => s.sessionId));
|
|
1285
1412
|
for (const bs of brainSessions) {
|
|
1286
1413
|
if (existingIds.has(bs.sessionId))
|
|
@@ -1365,18 +1492,22 @@ function handleCancelSearch(msg) {
|
|
|
1365
1492
|
searchControllers.delete(msg.searchId);
|
|
1366
1493
|
}
|
|
1367
1494
|
}
|
|
1368
|
-
function handleDeleteSession(sessionId) {
|
|
1495
|
+
async function handleDeleteSession(sessionId) {
|
|
1369
1496
|
// Evict any idle conversation holding this session; block if busy
|
|
1370
1497
|
if (evictByClaudeSessionId(sessionId)) {
|
|
1371
1498
|
send({ type: 'error', message: 'Cannot delete a session while it is processing.' });
|
|
1372
1499
|
return;
|
|
1373
1500
|
}
|
|
1501
|
+
if (!runtime) {
|
|
1502
|
+
send({ type: 'error', message: 'Session not found or could not be deleted.' });
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1374
1505
|
// Try current workDir first; if not found, check if it's a recap/briefing/devops/project session in BrainData
|
|
1375
|
-
let deleted =
|
|
1506
|
+
let deleted = await runtime.deleteHistorySession(state.workDir, sessionId);
|
|
1376
1507
|
if (!deleted) {
|
|
1377
1508
|
const meta = loadSessionMetadata(sessionId);
|
|
1378
1509
|
if (meta.recapId || meta.briefingDate || meta.devopsEntityType || meta.projectName || meta.icmId) {
|
|
1379
|
-
deleted =
|
|
1510
|
+
deleted = await runtime.deleteHistorySession(BRAIN_DATA_DIR, sessionId);
|
|
1380
1511
|
}
|
|
1381
1512
|
}
|
|
1382
1513
|
if (deleted) {
|
|
@@ -1387,13 +1518,17 @@ function handleDeleteSession(sessionId) {
|
|
|
1387
1518
|
send({ type: 'error', message: 'Session not found or could not be deleted.' });
|
|
1388
1519
|
}
|
|
1389
1520
|
}
|
|
1390
|
-
function handleRenameSession(sessionId, newTitle) {
|
|
1521
|
+
async function handleRenameSession(sessionId, newTitle) {
|
|
1522
|
+
if (!runtime) {
|
|
1523
|
+
send({ type: 'error', message: 'Session not found or could not be renamed.' });
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1391
1526
|
// Try current workDir first; if not found, check if it's a recap/briefing/devops/project session in BrainData
|
|
1392
|
-
let renamed =
|
|
1527
|
+
let renamed = await runtime.renameHistorySession(state.workDir, sessionId, newTitle);
|
|
1393
1528
|
if (!renamed) {
|
|
1394
1529
|
const meta = loadSessionMetadata(sessionId);
|
|
1395
1530
|
if (meta.recapId || meta.briefingDate || meta.devopsEntityType || meta.projectName || meta.icmId) {
|
|
1396
|
-
renamed =
|
|
1531
|
+
renamed = await runtime.renameHistorySession(BRAIN_DATA_DIR, sessionId, newTitle);
|
|
1397
1532
|
}
|
|
1398
1533
|
}
|
|
1399
1534
|
if (renamed) {
|
|
@@ -1403,15 +1538,20 @@ function handleRenameSession(sessionId, newTitle) {
|
|
|
1403
1538
|
// Session file may not exist on disk yet (e.g. IcM/project chat still streaming first response).
|
|
1404
1539
|
// Retry once after a short delay; if it still fails, send an error so the UI can recover.
|
|
1405
1540
|
setTimeout(() => {
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1541
|
+
(async () => {
|
|
1542
|
+
const retried = (await runtime.renameHistorySession(state.workDir, sessionId, newTitle))
|
|
1543
|
+
|| (await runtime.renameHistorySession(BRAIN_DATA_DIR, sessionId, newTitle));
|
|
1544
|
+
if (retried) {
|
|
1545
|
+
send({ type: 'session_renamed', sessionId, newTitle });
|
|
1546
|
+
}
|
|
1547
|
+
else {
|
|
1548
|
+
console.warn(`[AgentLink] rename_session: session ${sessionId.slice(0, 8)} not found after retry`);
|
|
1549
|
+
send({ type: 'error', message: 'Session not found or could not be renamed.' });
|
|
1550
|
+
}
|
|
1551
|
+
})().catch((err) => {
|
|
1552
|
+
console.error('[AgentLink] rename_session retry failed:', err);
|
|
1413
1553
|
send({ type: 'error', message: 'Session not found or could not be renamed.' });
|
|
1414
|
-
}
|
|
1554
|
+
});
|
|
1415
1555
|
}, 1500);
|
|
1416
1556
|
}
|
|
1417
1557
|
}
|