@agent-link/agent 0.1.204 → 0.1.207
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 +304 -136
- 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) {
|
|
@@ -464,7 +519,7 @@ function handleServerMessage(msg) {
|
|
|
464
519
|
const projectName = msg.projectName;
|
|
465
520
|
const icmId = msg.icmId;
|
|
466
521
|
const actionItemId = msg.actionItemId;
|
|
467
|
-
const
|
|
522
|
+
const explicitWorkDir = msg.workDir;
|
|
468
523
|
const effectiveBrainMode = !!isBrainMode;
|
|
469
524
|
console.log(`[AgentLink] chat: conversationId=${chatConvId}, existingConv.planMode=${existingConv?.planMode}, brainMode=${effectiveBrainMode}`);
|
|
470
525
|
const chatOptions = {
|
|
@@ -494,19 +549,56 @@ function handleServerMessage(msg) {
|
|
|
494
549
|
if (icmId) {
|
|
495
550
|
chatOptions.icmId = String(icmId);
|
|
496
551
|
}
|
|
497
|
-
const chatDir =
|
|
552
|
+
const chatDir = explicitWorkDir
|
|
553
|
+
?? existingConv?.workDir
|
|
554
|
+
?? ((recapId || briefingDate || devopsEntityType || projectName || icmId) ? BRAIN_DATA_DIR : null)
|
|
555
|
+
?? state.workDir;
|
|
498
556
|
// Track actionItemId for linking to Claude session on session_started
|
|
499
557
|
if (actionItemId && chatConvId) {
|
|
500
558
|
pendingActionItems.set(chatConvId, actionItemId);
|
|
501
559
|
}
|
|
502
|
-
|
|
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
|
+
});
|
|
503
583
|
break;
|
|
504
584
|
}
|
|
505
|
-
case 'cancel_execution':
|
|
506
|
-
|
|
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
|
+
});
|
|
507
595
|
break;
|
|
596
|
+
}
|
|
508
597
|
case 'list_sessions':
|
|
509
|
-
handleListSessions()
|
|
598
|
+
handleListSessions().catch((err) => {
|
|
599
|
+
console.error('[AgentLink] handleListSessions failed:', err);
|
|
600
|
+
send({ type: 'sessions_list', sessions: [], workDir: state.workDir });
|
|
601
|
+
});
|
|
510
602
|
break;
|
|
511
603
|
case 'list_recent_sessions':
|
|
512
604
|
handleListRecentSessions(msg);
|
|
@@ -571,6 +663,10 @@ function handleServerMessage(msg) {
|
|
|
571
663
|
// Backward compat: old web client sends this to reset the single conversation
|
|
572
664
|
abortClaude();
|
|
573
665
|
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');
|
|
574
670
|
console.log('[AgentLink] New conversation — session cleared');
|
|
575
671
|
break;
|
|
576
672
|
case 'resume_conversation': {
|
|
@@ -588,87 +684,148 @@ function handleServerMessage(msg) {
|
|
|
588
684
|
// (handles page refresh where web client generates a new UUID)
|
|
589
685
|
rebindConversation(m.claudeSessionId, convId);
|
|
590
686
|
}
|
|
591
|
-
//
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
+
}
|
|
597
706
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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 });
|
|
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();
|
|
617
716
|
}
|
|
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
|
-
}
|
|
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;
|
|
642
744
|
}
|
|
643
|
-
//
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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(() => { });
|
|
655
761
|
}
|
|
656
|
-
|
|
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
|
+
}
|
|
799
|
+
}
|
|
800
|
+
})().catch((err) => {
|
|
801
|
+
console.error('[AgentLink] resume_conversation failed:', err);
|
|
802
|
+
send({ type: 'error', message: 'Failed to resume conversation.' });
|
|
803
|
+
});
|
|
657
804
|
break;
|
|
658
805
|
}
|
|
659
806
|
case 'ask_user_answer': {
|
|
660
807
|
const m = msg;
|
|
661
|
-
|
|
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);
|
|
662
813
|
break;
|
|
663
814
|
}
|
|
664
815
|
case 'delete_session': {
|
|
665
816
|
const m = msg;
|
|
666
|
-
handleDeleteSession(m.sessionId)
|
|
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
|
+
});
|
|
667
821
|
break;
|
|
668
822
|
}
|
|
669
823
|
case 'rename_session': {
|
|
670
824
|
const m = msg;
|
|
671
|
-
handleRenameSession(m.sessionId, m.newTitle)
|
|
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
|
+
});
|
|
672
829
|
break;
|
|
673
830
|
}
|
|
674
831
|
case 'query_active_conversations': {
|
|
@@ -679,7 +836,7 @@ function handleServerMessage(msg) {
|
|
|
679
836
|
conversationId: convId,
|
|
680
837
|
claudeSessionId: conv.claudeSessionId,
|
|
681
838
|
isProcessing: true,
|
|
682
|
-
isCompacting:
|
|
839
|
+
isCompacting: runtime?.isCompacting(convId) ?? false,
|
|
683
840
|
});
|
|
684
841
|
}
|
|
685
842
|
else if (conv.claudeSessionId) {
|
|
@@ -793,11 +950,11 @@ function handleServerMessage(msg) {
|
|
|
793
950
|
break;
|
|
794
951
|
}
|
|
795
952
|
console.log(`[AgentLink] set_model: model=${model}, conversationId=${conversationId}`);
|
|
796
|
-
setModel(conversationId, model);
|
|
953
|
+
runtime?.setModel(conversationId ?? null, model);
|
|
797
954
|
// Kill current Claude process so next message spawns with new --model flag
|
|
798
955
|
const conv = getConversation(conversationId);
|
|
799
956
|
if (conv) {
|
|
800
|
-
restartConversation(conversationId);
|
|
957
|
+
runtime?.restartConversation(conversationId ?? 'default');
|
|
801
958
|
}
|
|
802
959
|
send({ type: 'model_changed', model, conversationId });
|
|
803
960
|
break;
|
|
@@ -805,17 +962,10 @@ function handleServerMessage(msg) {
|
|
|
805
962
|
case 'set_plan_mode': {
|
|
806
963
|
const { enabled, conversationId } = msg;
|
|
807
964
|
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 });
|
|
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 });
|
|
819
969
|
}
|
|
820
970
|
send({ type: 'plan_mode_changed', enabled, conversationId });
|
|
821
971
|
break;
|
|
@@ -823,22 +973,10 @@ function handleServerMessage(msg) {
|
|
|
823
973
|
case 'set_permission_mode': {
|
|
824
974
|
const { mode, conversationId } = msg;
|
|
825
975
|
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
|
-
});
|
|
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 });
|
|
842
980
|
}
|
|
843
981
|
send({ type: 'permission_mode_changed', mode, conversationId });
|
|
844
982
|
// Persist permission mode for this conversation's session
|
|
@@ -855,23 +993,20 @@ function handleServerMessage(msg) {
|
|
|
855
993
|
case 'tool_permission_response': {
|
|
856
994
|
const { requestId, behavior } = msg;
|
|
857
995
|
console.log(`[AgentLink] tool_permission_response: ${behavior} (${requestId})`);
|
|
858
|
-
|
|
996
|
+
if (!runtime) {
|
|
997
|
+
console.warn('[AgentLink] tool_permission_response received after runtime shutdown — ignoring');
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
runtime.answerApproval(requestId, behavior);
|
|
859
1001
|
break;
|
|
860
1002
|
}
|
|
861
1003
|
case 'set_brain_mode': {
|
|
862
1004
|
const { enabled, conversationId } = msg;
|
|
863
1005
|
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 });
|
|
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 });
|
|
875
1010
|
}
|
|
876
1011
|
send({ type: 'brain_mode_changed', enabled, conversationId });
|
|
877
1012
|
break;
|
|
@@ -1269,18 +1404,38 @@ async function handleBrainChatDeleteSession(msg) {
|
|
|
1269
1404
|
send({ type: 'brainchat_error', message: String(err) });
|
|
1270
1405
|
}
|
|
1271
1406
|
}
|
|
1272
|
-
function handleListSessions() {
|
|
1407
|
+
async function handleListSessions() {
|
|
1273
1408
|
try {
|
|
1274
|
-
|
|
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
|
+
}));
|
|
1275
1423
|
const metaMap = loadAllSessionMetadata();
|
|
1276
1424
|
const enriched = sessions.map(s => ({
|
|
1277
1425
|
...s,
|
|
1278
1426
|
...metaMap.get(s.sessionId),
|
|
1279
1427
|
}));
|
|
1280
1428
|
// Always merge recap/briefing/devops chat sessions from BrainData directory — they live under a
|
|
1281
|
-
// different Claude project folder, so
|
|
1429
|
+
// different Claude project folder, so listHistory(state.workDir) misses them.
|
|
1282
1430
|
// These sessions should be visible regardless of the current workDir.
|
|
1283
|
-
const
|
|
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
|
+
}));
|
|
1284
1439
|
const existingIds = new Set(enriched.map(s => s.sessionId));
|
|
1285
1440
|
for (const bs of brainSessions) {
|
|
1286
1441
|
if (existingIds.has(bs.sessionId))
|
|
@@ -1365,18 +1520,22 @@ function handleCancelSearch(msg) {
|
|
|
1365
1520
|
searchControllers.delete(msg.searchId);
|
|
1366
1521
|
}
|
|
1367
1522
|
}
|
|
1368
|
-
function handleDeleteSession(sessionId) {
|
|
1523
|
+
async function handleDeleteSession(sessionId) {
|
|
1369
1524
|
// Evict any idle conversation holding this session; block if busy
|
|
1370
1525
|
if (evictByClaudeSessionId(sessionId)) {
|
|
1371
1526
|
send({ type: 'error', message: 'Cannot delete a session while it is processing.' });
|
|
1372
1527
|
return;
|
|
1373
1528
|
}
|
|
1529
|
+
if (!runtime) {
|
|
1530
|
+
send({ type: 'error', message: 'Session not found or could not be deleted.' });
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1374
1533
|
// Try current workDir first; if not found, check if it's a recap/briefing/devops/project session in BrainData
|
|
1375
|
-
let deleted =
|
|
1534
|
+
let deleted = await runtime.deleteHistorySession(state.workDir, sessionId);
|
|
1376
1535
|
if (!deleted) {
|
|
1377
1536
|
const meta = loadSessionMetadata(sessionId);
|
|
1378
1537
|
if (meta.recapId || meta.briefingDate || meta.devopsEntityType || meta.projectName || meta.icmId) {
|
|
1379
|
-
deleted =
|
|
1538
|
+
deleted = await runtime.deleteHistorySession(BRAIN_DATA_DIR, sessionId);
|
|
1380
1539
|
}
|
|
1381
1540
|
}
|
|
1382
1541
|
if (deleted) {
|
|
@@ -1387,13 +1546,17 @@ function handleDeleteSession(sessionId) {
|
|
|
1387
1546
|
send({ type: 'error', message: 'Session not found or could not be deleted.' });
|
|
1388
1547
|
}
|
|
1389
1548
|
}
|
|
1390
|
-
function handleRenameSession(sessionId, newTitle) {
|
|
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
|
+
}
|
|
1391
1554
|
// Try current workDir first; if not found, check if it's a recap/briefing/devops/project session in BrainData
|
|
1392
|
-
let renamed =
|
|
1555
|
+
let renamed = await runtime.renameHistorySession(state.workDir, sessionId, newTitle);
|
|
1393
1556
|
if (!renamed) {
|
|
1394
1557
|
const meta = loadSessionMetadata(sessionId);
|
|
1395
1558
|
if (meta.recapId || meta.briefingDate || meta.devopsEntityType || meta.projectName || meta.icmId) {
|
|
1396
|
-
renamed =
|
|
1559
|
+
renamed = await runtime.renameHistorySession(BRAIN_DATA_DIR, sessionId, newTitle);
|
|
1397
1560
|
}
|
|
1398
1561
|
}
|
|
1399
1562
|
if (renamed) {
|
|
@@ -1403,15 +1566,20 @@ function handleRenameSession(sessionId, newTitle) {
|
|
|
1403
1566
|
// Session file may not exist on disk yet (e.g. IcM/project chat still streaming first response).
|
|
1404
1567
|
// Retry once after a short delay; if it still fails, send an error so the UI can recover.
|
|
1405
1568
|
setTimeout(() => {
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
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);
|
|
1413
1581
|
send({ type: 'error', message: 'Session not found or could not be renamed.' });
|
|
1414
|
-
}
|
|
1582
|
+
});
|
|
1415
1583
|
}, 1500);
|
|
1416
1584
|
}
|
|
1417
1585
|
}
|