@agent-link/agent 0.1.210 → 0.1.212
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/connection.d.ts +0 -11
- package/dist/connection.js +109 -277
- package/dist/connection.js.map +1 -1
- package/dist/directory-handlers.d.ts +0 -7
- package/dist/directory-handlers.js +6 -12
- package/dist/directory-handlers.js.map +1 -1
- package/dist/history.d.ts +0 -15
- package/dist/history.js +0 -49
- package/dist/history.js.map +1 -1
- package/dist/runtime.d.ts +0 -15
- package/dist/runtime.js +0 -30
- package/dist/runtime.js.map +1 -1
- package/package.json +1 -1
package/dist/connection.js
CHANGED
|
@@ -3,7 +3,7 @@ import os from 'os';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
5
|
import { loadRuntimeState, saveRuntimeState, getBrainDataDir, getConversationPermission, setConversationPermission, clearConversationPermission, getOrCreateE2EKey, encodeBase64Url } from './config.js';
|
|
6
|
-
import { handleListDirectory, handleReadFile, handleChangeWorkDir, handleUpdateFile, handleCreateFile, handleCreateDirectory, handleDeleteFile, handleUploadFile, handleUploadFileChunk, handleDownloadFile, handleDownloadChunkAck
|
|
6
|
+
import { handleListDirectory, handleReadFile, handleChangeWorkDir, handleUpdateFile, handleCreateFile, handleCreateDirectory, handleDeleteFile, handleUploadFile, handleUploadFileChunk, handleDownloadFile, handleDownloadChunkAck } from './directory-handlers.js';
|
|
7
7
|
import { handleGitStatus, handleGitDiff, handleGitStage, handleGitUnstage, handleGitDiscard, handleGitCommit, handleGitWorktreeList, handleGitStash, handleGitPull, handleGitPush, handleGitDeleteFile, resolveToRepoRoot } from './git-handlers.js';
|
|
8
8
|
import { handleCreateTeam, handleDissolveTeam, handleListTeams, handleGetTeam, handleGetTeamAgentHistory, handleDeleteTeam, handleRenameTeam } from './team-handlers.js';
|
|
9
9
|
import { handleCreateLoop, handleUpdateLoop, handleDeleteLoop, handleListLoops, handleGetLoop, handleRunLoop, handleCancelLoopExecution, handleListLoopExecutions, handleGetLoopExecutionMessages, handleQueryLoopStatus } from './loop-handlers.js';
|
|
@@ -20,7 +20,7 @@ import { createTerminalManager } from './terminal.js';
|
|
|
20
20
|
const require = createRequire(import.meta.url);
|
|
21
21
|
const pkg = require('../package.json');
|
|
22
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
|
|
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';
|
|
@@ -34,25 +34,6 @@ const RECONNECT_BASE_DELAY = 1000;
|
|
|
34
34
|
const RECONNECT_MAX_DELAY = 10_000;
|
|
35
35
|
const HEARTBEAT_INTERVAL = 45_000; // Send ping every 45s (staggered from server's 30s)
|
|
36
36
|
const HEARTBEAT_TIMEOUT = 15_000; // Max wait for pong before declaring dead
|
|
37
|
-
/**
|
|
38
|
-
* Resolve the workDir for an inbound message. If the message carries a
|
|
39
|
-
* `conversationId` and the runtime has a per-conv workDir mapped for it,
|
|
40
|
-
* use that; otherwise fall back to the agent-global `state.workDir`.
|
|
41
|
-
*/
|
|
42
|
-
function resolveWorkDir(msg, st, rt) {
|
|
43
|
-
const convId = msg?.conversationId;
|
|
44
|
-
if (typeof convId === 'string' && convId && rt) {
|
|
45
|
-
const perConv = rt.getConversationWorkDir(convId);
|
|
46
|
-
if (perConv)
|
|
47
|
-
return perConv;
|
|
48
|
-
}
|
|
49
|
-
return st.workDir;
|
|
50
|
-
}
|
|
51
|
-
function tag(sendFn, convId) {
|
|
52
|
-
if (!convId)
|
|
53
|
-
return sendFn;
|
|
54
|
-
return (m) => sendFn({ ...m, conversationId: convId });
|
|
55
|
-
}
|
|
56
37
|
const state = {
|
|
57
38
|
ws: null,
|
|
58
39
|
sessionId: null,
|
|
@@ -420,19 +401,7 @@ export function disconnect() {
|
|
|
420
401
|
return r ? r.shutdown() : Promise.resolve();
|
|
421
402
|
}
|
|
422
403
|
let sendQueue = Promise.resolve();
|
|
423
|
-
/** Test-only override: when set, send() routes through this fn instead of WebSocket. */
|
|
424
|
-
let testSendOverride = null;
|
|
425
|
-
export function _setTestSendOverride(fn) {
|
|
426
|
-
testSendOverride = fn;
|
|
427
|
-
}
|
|
428
404
|
export function send(msg) {
|
|
429
|
-
if (testSendOverride) {
|
|
430
|
-
try {
|
|
431
|
-
testSendOverride(msg);
|
|
432
|
-
}
|
|
433
|
-
catch { /* swallow in tests */ }
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
405
|
if (state.ws && state.ws.readyState === WebSocket.OPEN) {
|
|
437
406
|
sendQueue = sendQueue.then(async () => {
|
|
438
407
|
if (!state.ws || state.ws.readyState !== WebSocket.OPEN)
|
|
@@ -471,9 +440,7 @@ function buildWsUrl(config) {
|
|
|
471
440
|
name: config.name,
|
|
472
441
|
workDir: state.workDir,
|
|
473
442
|
hostname: os.hostname(),
|
|
474
|
-
|
|
475
|
-
// so the web's MIN_AGENT_VERSION_PER_CONV_WORKDIR gate triggers fallback.
|
|
476
|
-
version: process.env.AGENTLINK_FAKE_VERSION || pkg.version,
|
|
443
|
+
version: pkg.version,
|
|
477
444
|
});
|
|
478
445
|
// On reconnect, send previous sessionId so the URL stays valid
|
|
479
446
|
if (state.sessionId) {
|
|
@@ -515,21 +482,6 @@ const BRAIN_SERVER = 'http://localhost:8001';
|
|
|
515
482
|
// Pending action item IDs: conversationId → actionItemId
|
|
516
483
|
// Used to link a chat's Claude session to an action item for CLI start
|
|
517
484
|
const pendingActionItems = new Map();
|
|
518
|
-
/** Test-only: invoke the dispatch directly. */
|
|
519
|
-
export function _handleServerMessageForTests(msg) {
|
|
520
|
-
handleServerMessage(msg);
|
|
521
|
-
}
|
|
522
|
-
/** Test-only: read/write the connection-level workDir state. */
|
|
523
|
-
export function _setStateWorkDirForTests(wd) {
|
|
524
|
-
state.workDir = wd;
|
|
525
|
-
}
|
|
526
|
-
export function _getStateWorkDirForTests() {
|
|
527
|
-
return state.workDir;
|
|
528
|
-
}
|
|
529
|
-
/** Test-only: install a runtime instance (bypasses connect()). */
|
|
530
|
-
export function _setRuntimeForTests(rt) {
|
|
531
|
-
runtime = rt;
|
|
532
|
-
}
|
|
533
485
|
function handleServerMessage(msg) {
|
|
534
486
|
console.log(`[AgentLink] ← ${msg.type}`);
|
|
535
487
|
switch (msg.type) {
|
|
@@ -600,7 +552,7 @@ function handleServerMessage(msg) {
|
|
|
600
552
|
const chatDir = explicitWorkDir
|
|
601
553
|
?? existingConv?.workDir
|
|
602
554
|
?? ((recapId || briefingDate || devopsEntityType || projectName || icmId) ? BRAIN_DATA_DIR : null)
|
|
603
|
-
??
|
|
555
|
+
?? state.workDir;
|
|
604
556
|
// Track actionItemId for linking to Claude session on session_started
|
|
605
557
|
if (actionItemId && chatConvId) {
|
|
606
558
|
pendingActionItems.set(chatConvId, actionItemId);
|
|
@@ -621,9 +573,6 @@ function handleServerMessage(msg) {
|
|
|
621
573
|
console.warn('[AgentLink] chat received after runtime shutdown — ignoring');
|
|
622
574
|
break;
|
|
623
575
|
}
|
|
624
|
-
// Seed per-conv workDir map so subsequent resolveWorkDir calls for this
|
|
625
|
-
// conversation see the same dir (no eviction — this is the active turn).
|
|
626
|
-
runtime.setConversationWorkDir(startConvId, chatDir);
|
|
627
576
|
runtime.startTurn(startConvId, {
|
|
628
577
|
text: msg.prompt,
|
|
629
578
|
files: msg.files,
|
|
@@ -645,18 +594,12 @@ function handleServerMessage(msg) {
|
|
|
645
594
|
});
|
|
646
595
|
break;
|
|
647
596
|
}
|
|
648
|
-
case 'list_sessions':
|
|
649
|
-
|
|
650
|
-
const wd = resolveWorkDir(msg, state, runtime);
|
|
651
|
-
handleListSessions(wd, m.conversationId).catch((err) => {
|
|
597
|
+
case 'list_sessions':
|
|
598
|
+
handleListSessions().catch((err) => {
|
|
652
599
|
console.error('[AgentLink] handleListSessions failed:', err);
|
|
653
|
-
|
|
654
|
-
if (m.conversationId)
|
|
655
|
-
out.conversationId = m.conversationId;
|
|
656
|
-
send(out);
|
|
600
|
+
send({ type: 'sessions_list', sessions: [], workDir: state.workDir });
|
|
657
601
|
});
|
|
658
602
|
break;
|
|
659
|
-
}
|
|
660
603
|
case 'list_recent_sessions':
|
|
661
604
|
handleListRecentSessions(msg);
|
|
662
605
|
break;
|
|
@@ -666,102 +609,56 @@ function handleServerMessage(msg) {
|
|
|
666
609
|
case 'cancel_search':
|
|
667
610
|
handleCancelSearch(msg);
|
|
668
611
|
break;
|
|
669
|
-
case 'list_directory':
|
|
670
|
-
|
|
671
|
-
handleListDirectory(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
612
|
+
case 'list_directory':
|
|
613
|
+
handleListDirectory(msg, state.workDir, send);
|
|
672
614
|
break;
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const m = msg;
|
|
676
|
-
handleReadFile(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
615
|
+
case 'read_file':
|
|
616
|
+
handleReadFile(msg, state.workDir, send);
|
|
677
617
|
break;
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
const m = msg;
|
|
681
|
-
const tagged = tag(send, m.conversationId);
|
|
682
|
-
resolveToRepoRoot(m, resolveWorkDir(msg, state, runtime))
|
|
618
|
+
case 'git_read_file':
|
|
619
|
+
resolveToRepoRoot(msg, state.workDir)
|
|
683
620
|
.then((repoRoot) => {
|
|
684
|
-
const filePath =
|
|
621
|
+
const filePath = msg.filePath;
|
|
685
622
|
// Security: reject absolute paths and .. traversal
|
|
686
623
|
if (path.isAbsolute(filePath) || filePath.includes('..')) {
|
|
687
|
-
|
|
624
|
+
send({ type: 'file_content', filePath, error: 'Invalid file path' });
|
|
688
625
|
return;
|
|
689
626
|
}
|
|
690
627
|
const resolved = path.resolve(repoRoot, filePath);
|
|
691
628
|
if (!resolved.startsWith(repoRoot)) {
|
|
692
|
-
|
|
629
|
+
send({ type: 'file_content', filePath, error: 'Invalid file path' });
|
|
693
630
|
return;
|
|
694
631
|
}
|
|
695
|
-
handleReadFile({ filePath }, repoRoot,
|
|
632
|
+
handleReadFile({ filePath }, repoRoot, send);
|
|
696
633
|
});
|
|
697
634
|
break;
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const m = msg;
|
|
701
|
-
handleUpdateFile(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
635
|
+
case 'update_file':
|
|
636
|
+
handleUpdateFile(msg, state.workDir, send);
|
|
702
637
|
break;
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const m = msg;
|
|
706
|
-
handleCreateFile(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
638
|
+
case 'create_file':
|
|
639
|
+
handleCreateFile(msg, state.workDir, send);
|
|
707
640
|
break;
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const m = msg;
|
|
711
|
-
handleCreateDirectory(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
641
|
+
case 'create_directory':
|
|
642
|
+
handleCreateDirectory(msg, state.workDir, send);
|
|
712
643
|
break;
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const m = msg;
|
|
716
|
-
handleDeleteFile(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
644
|
+
case 'delete_file':
|
|
645
|
+
handleDeleteFile(msg, state.workDir, send);
|
|
717
646
|
break;
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
const m = msg;
|
|
721
|
-
handleUploadFile(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
647
|
+
case 'upload_file':
|
|
648
|
+
handleUploadFile(msg, state.workDir, send);
|
|
722
649
|
break;
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
const m = msg;
|
|
726
|
-
handleUploadFileChunk(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
650
|
+
case 'upload_file_chunk':
|
|
651
|
+
handleUploadFileChunk(msg, state.workDir, send);
|
|
727
652
|
break;
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
const m = msg;
|
|
731
|
-
handleDownloadFile(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
653
|
+
case 'download_file':
|
|
654
|
+
handleDownloadFile(msg, state.workDir, send);
|
|
732
655
|
break;
|
|
733
|
-
}
|
|
734
656
|
case 'download_chunk_ack':
|
|
735
657
|
handleDownloadChunkAck(msg);
|
|
736
658
|
break;
|
|
737
659
|
case 'change_workdir':
|
|
738
|
-
handleChangeWorkDir(msg, state, send,
|
|
739
|
-
handleListSessions(state.workDir).catch((err) => {
|
|
740
|
-
console.error('[AgentLink] handleListSessions failed:', err);
|
|
741
|
-
send({ type: 'sessions_list', sessions: [], workDir: state.workDir });
|
|
742
|
-
});
|
|
743
|
-
});
|
|
744
|
-
break;
|
|
745
|
-
case 'set_conversation_workdir': {
|
|
746
|
-
const m = msg;
|
|
747
|
-
const convId = m.conversationId;
|
|
748
|
-
if (typeof convId !== 'string' || !convId) {
|
|
749
|
-
send({ type: 'error', message: 'set_conversation_workdir requires conversationId' });
|
|
750
|
-
break;
|
|
751
|
-
}
|
|
752
|
-
const v = validateWorkDir(m.workDir);
|
|
753
|
-
if (!v.ok) {
|
|
754
|
-
send({ type: 'error', conversationId: convId, message: v.error });
|
|
755
|
-
break;
|
|
756
|
-
}
|
|
757
|
-
runtime?.setConversationWorkDir(convId, v.resolved, { evictSession: true });
|
|
758
|
-
// Also update the global default so NEW conversations (with a fresh
|
|
759
|
-
// convId that has no per-conv mapping) inherit the user's latest
|
|
760
|
-
// directory choice instead of falling back to the agent startup dir.
|
|
761
|
-
state.workDir = v.resolved;
|
|
762
|
-
send({ type: 'conversation_workdir_changed', conversationId: convId, workDir: v.resolved });
|
|
660
|
+
handleChangeWorkDir(msg, state, send, handleListSessions);
|
|
763
661
|
break;
|
|
764
|
-
}
|
|
765
662
|
case 'new_conversation':
|
|
766
663
|
// Backward compat: old web client sends this to reset the single conversation
|
|
767
664
|
abortClaude();
|
|
@@ -792,18 +689,10 @@ function handleServerMessage(msg) {
|
|
|
792
689
|
// making the entire switch dispatch async. .catch() ensures backend
|
|
793
690
|
// rejections don't become unhandled promise rejections.
|
|
794
691
|
(async () => {
|
|
795
|
-
// Try current workDir first; fall back to BRAIN_DATA_DIR for recap/briefing chat sessions
|
|
796
|
-
|
|
797
|
-
// resume_conversation message with conversationId (see outboundTag.js).
|
|
798
|
-
const initialWorkDir = resolveWorkDir(msg, state, runtime);
|
|
799
|
-
// Deeplink fix: when the web client opens a URL like #/chat/<sessionId>
|
|
800
|
-
// in a fresh tab, it has no projectPath context, so resolveWorkDir
|
|
801
|
-
// returns the agent's startup workDir — which may be wrong for this
|
|
802
|
-
// session. Prefer the authoritative `cwd` recorded in the JSONL itself.
|
|
803
|
-
const jsonlCwd = await findSessionProjectPath(m.claudeSessionId);
|
|
804
|
-
let resolvedWorkDir = jsonlCwd || initialWorkDir;
|
|
692
|
+
// Try current workDir first; fall back to BRAIN_DATA_DIR for recap/briefing chat sessions
|
|
693
|
+
let resolvedWorkDir = state.workDir;
|
|
805
694
|
let history = runtime
|
|
806
|
-
? await runtime.readHistory(
|
|
695
|
+
? await runtime.readHistory(state.workDir, m.claudeSessionId)
|
|
807
696
|
: [];
|
|
808
697
|
if (history.length === 0) {
|
|
809
698
|
const sessionMeta_ = loadSessionMetadata(m.claudeSessionId);
|
|
@@ -864,13 +753,8 @@ function handleServerMessage(msg) {
|
|
|
864
753
|
// process's own session id).
|
|
865
754
|
const seedCid = convId ?? 'default';
|
|
866
755
|
const seedConv = currentConv;
|
|
867
|
-
if (!seedConv?.turnActive
|
|
868
|
-
|
|
869
|
-
// The web client re-requests list_sessions immediately after receiving
|
|
870
|
-
// conversation_resumed (when workDir changed), and list_sessions uses
|
|
871
|
-
// resolveWorkDir which reads from this per-conv map. Without awaiting,
|
|
872
|
-
// the map is still empty and resolveWorkDir falls back to startup dir.
|
|
873
|
-
await runtime.ensureConversation(seedCid, {
|
|
756
|
+
if (!seedConv?.turnActive) {
|
|
757
|
+
runtime?.ensureConversation(seedCid, {
|
|
874
758
|
workDir: resolvedWorkDir,
|
|
875
759
|
resumeSessionId: m.claudeSessionId,
|
|
876
760
|
}).catch(() => { });
|
|
@@ -879,7 +763,6 @@ function handleServerMessage(msg) {
|
|
|
879
763
|
type: 'conversation_resumed',
|
|
880
764
|
conversationId: convId,
|
|
881
765
|
claudeSessionId: m.claudeSessionId,
|
|
882
|
-
workDir: resolvedWorkDir,
|
|
883
766
|
history,
|
|
884
767
|
isCompacting: isSameSession && (runtime?.isCompacting(convId) ?? false),
|
|
885
768
|
isProcessing: isSameSession && currentConv?.turnActive === true,
|
|
@@ -931,23 +814,17 @@ function handleServerMessage(msg) {
|
|
|
931
814
|
}
|
|
932
815
|
case 'delete_session': {
|
|
933
816
|
const m = msg;
|
|
934
|
-
handleDeleteSession(m.sessionId
|
|
817
|
+
handleDeleteSession(m.sessionId).catch((err) => {
|
|
935
818
|
console.error('[AgentLink] handleDeleteSession failed:', err);
|
|
936
|
-
|
|
937
|
-
if (m.conversationId)
|
|
938
|
-
out.conversationId = m.conversationId;
|
|
939
|
-
send(out);
|
|
819
|
+
send({ type: 'error', message: 'Session not found or could not be deleted.' });
|
|
940
820
|
});
|
|
941
821
|
break;
|
|
942
822
|
}
|
|
943
823
|
case 'rename_session': {
|
|
944
824
|
const m = msg;
|
|
945
|
-
handleRenameSession(m.sessionId, m.newTitle
|
|
825
|
+
handleRenameSession(m.sessionId, m.newTitle).catch((err) => {
|
|
946
826
|
console.error('[AgentLink] handleRenameSession failed:', err);
|
|
947
|
-
|
|
948
|
-
if (m.conversationId)
|
|
949
|
-
out.conversationId = m.conversationId;
|
|
950
|
-
send(out);
|
|
827
|
+
send({ type: 'error', message: 'Session not found or could not be renamed.' });
|
|
951
828
|
});
|
|
952
829
|
break;
|
|
953
830
|
}
|
|
@@ -988,19 +865,15 @@ function handleServerMessage(msg) {
|
|
|
988
865
|
});
|
|
989
866
|
break;
|
|
990
867
|
}
|
|
991
|
-
case 'create_team':
|
|
992
|
-
|
|
993
|
-
handleCreateTeam(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
868
|
+
case 'create_team':
|
|
869
|
+
handleCreateTeam(msg, state.workDir, send);
|
|
994
870
|
break;
|
|
995
|
-
}
|
|
996
871
|
case 'dissolve_team':
|
|
997
872
|
handleDissolveTeam();
|
|
998
873
|
break;
|
|
999
|
-
case 'list_teams':
|
|
1000
|
-
|
|
1001
|
-
handleListTeams(resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
874
|
+
case 'list_teams':
|
|
875
|
+
handleListTeams(state.workDir, send);
|
|
1002
876
|
break;
|
|
1003
|
-
}
|
|
1004
877
|
case 'get_team':
|
|
1005
878
|
handleGetTeam(msg, send);
|
|
1006
879
|
break;
|
|
@@ -1014,22 +887,18 @@ function handleServerMessage(msg) {
|
|
|
1014
887
|
handleRenameTeam(msg, send);
|
|
1015
888
|
break;
|
|
1016
889
|
// ── Loop (Scheduled Tasks) handlers ────────────────────────────────
|
|
1017
|
-
case 'create_loop':
|
|
1018
|
-
|
|
1019
|
-
handleCreateLoop(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
890
|
+
case 'create_loop':
|
|
891
|
+
handleCreateLoop(msg, state.workDir, send);
|
|
1020
892
|
break;
|
|
1021
|
-
}
|
|
1022
893
|
case 'update_loop':
|
|
1023
894
|
handleUpdateLoop(msg, send);
|
|
1024
895
|
break;
|
|
1025
896
|
case 'delete_loop':
|
|
1026
897
|
handleDeleteLoop(msg, send);
|
|
1027
898
|
break;
|
|
1028
|
-
case 'list_loops':
|
|
1029
|
-
|
|
1030
|
-
handleListLoops(m.showAll ? undefined : resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
899
|
+
case 'list_loops':
|
|
900
|
+
handleListLoops(msg.showAll ? undefined : state.workDir, send);
|
|
1031
901
|
break;
|
|
1032
|
-
}
|
|
1033
902
|
case 'get_loop':
|
|
1034
903
|
handleGetLoop(msg, send);
|
|
1035
904
|
break;
|
|
@@ -1052,26 +921,25 @@ function handleServerMessage(msg) {
|
|
|
1052
921
|
send({ type: 'pong', ts: msg.ts });
|
|
1053
922
|
break;
|
|
1054
923
|
case 'list_memory': {
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
tag(send, m.conversationId)({ type: 'memory_list', memoryDir: result.memoryDir, files: result.files });
|
|
924
|
+
const result = listMemoryFiles(state.workDir);
|
|
925
|
+
send({ type: 'memory_list', memoryDir: result.memoryDir, files: result.files });
|
|
1058
926
|
break;
|
|
1059
927
|
}
|
|
1060
928
|
case 'update_memory': {
|
|
1061
|
-
const { filename, content
|
|
1062
|
-
const result = updateMemoryFile(
|
|
1063
|
-
|
|
929
|
+
const { filename, content } = msg;
|
|
930
|
+
const result = updateMemoryFile(state.workDir, filename, content);
|
|
931
|
+
send({ type: 'memory_updated', filename, ...result });
|
|
1064
932
|
break;
|
|
1065
933
|
}
|
|
1066
934
|
case 'delete_memory': {
|
|
1067
|
-
const { filename
|
|
1068
|
-
const result = deleteMemoryFile(
|
|
1069
|
-
|
|
935
|
+
const { filename } = msg;
|
|
936
|
+
const result = deleteMemoryFile(state.workDir, filename);
|
|
937
|
+
send({ type: 'memory_deleted', filename, ...result });
|
|
1070
938
|
break;
|
|
1071
939
|
}
|
|
1072
940
|
case 'btw_question': {
|
|
1073
941
|
const { question, conversationId, claudeSessionId } = msg;
|
|
1074
|
-
handleBtwQuestion(question, conversationId,
|
|
942
|
+
handleBtwQuestion(question, conversationId, state.workDir, send, claudeSessionId);
|
|
1075
943
|
break;
|
|
1076
944
|
}
|
|
1077
945
|
case 'set_model': {
|
|
@@ -1095,7 +963,7 @@ function handleServerMessage(msg) {
|
|
|
1095
963
|
const { enabled, conversationId } = msg;
|
|
1096
964
|
console.log(`[AgentLink] set_plan_mode: enabled=${enabled}, conversationId=${conversationId}`);
|
|
1097
965
|
const convId = conversationId ?? 'default';
|
|
1098
|
-
const result = runtime?.setPlanMode(convId, enabled,
|
|
966
|
+
const result = runtime?.setPlanMode(convId, enabled, state.workDir) ?? null;
|
|
1099
967
|
if (result?.wasTurnActive) {
|
|
1100
968
|
send({ type: 'execution_cancelled', conversationId });
|
|
1101
969
|
}
|
|
@@ -1106,7 +974,7 @@ function handleServerMessage(msg) {
|
|
|
1106
974
|
const { mode, conversationId } = msg;
|
|
1107
975
|
console.log(`[AgentLink] set_permission_mode: mode=${mode}, conversationId=${conversationId}`);
|
|
1108
976
|
const convId = conversationId ?? 'default';
|
|
1109
|
-
const result = runtime?.setPermissionMode(convId, mode,
|
|
977
|
+
const result = runtime?.setPermissionMode(convId, mode, state.workDir) ?? null;
|
|
1110
978
|
if (result?.wasTurnActive) {
|
|
1111
979
|
send({ type: 'execution_cancelled', conversationId });
|
|
1112
980
|
}
|
|
@@ -1136,68 +1004,46 @@ function handleServerMessage(msg) {
|
|
|
1136
1004
|
const { enabled, conversationId } = msg;
|
|
1137
1005
|
console.log(`[AgentLink] set_brain_mode: enabled=${enabled}, conversationId=${conversationId}`);
|
|
1138
1006
|
const convId = conversationId ?? 'default';
|
|
1139
|
-
const result = runtime?.setBrainMode(convId, enabled,
|
|
1007
|
+
const result = runtime?.setBrainMode(convId, enabled, state.workDir) ?? null;
|
|
1140
1008
|
if (result?.wasTurnActive) {
|
|
1141
1009
|
send({ type: 'execution_cancelled', conversationId });
|
|
1142
1010
|
}
|
|
1143
1011
|
send({ type: 'brain_mode_changed', enabled, conversationId });
|
|
1144
1012
|
break;
|
|
1145
1013
|
}
|
|
1146
|
-
case 'git_worktree_list':
|
|
1147
|
-
|
|
1148
|
-
handleGitWorktreeList(msg, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
1014
|
+
case 'git_worktree_list':
|
|
1015
|
+
handleGitWorktreeList(msg, state.workDir, send);
|
|
1149
1016
|
break;
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
const m = msg;
|
|
1153
|
-
handleGitStatus(msg, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
1017
|
+
case 'git_status':
|
|
1018
|
+
handleGitStatus(msg, state.workDir, send);
|
|
1154
1019
|
break;
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
const m = msg;
|
|
1158
|
-
handleGitDiff(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
1020
|
+
case 'git_diff':
|
|
1021
|
+
handleGitDiff(msg, state.workDir, send);
|
|
1159
1022
|
break;
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
const m = msg;
|
|
1163
|
-
handleGitStage(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
1023
|
+
case 'git_stage':
|
|
1024
|
+
handleGitStage(msg, state.workDir, send);
|
|
1164
1025
|
break;
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
const m = msg;
|
|
1168
|
-
handleGitUnstage(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
1026
|
+
case 'git_unstage':
|
|
1027
|
+
handleGitUnstage(msg, state.workDir, send);
|
|
1169
1028
|
break;
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
const m = msg;
|
|
1173
|
-
handleGitDiscard(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
1029
|
+
case 'git_discard':
|
|
1030
|
+
handleGitDiscard(msg, state.workDir, send);
|
|
1174
1031
|
break;
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
const m = msg;
|
|
1178
|
-
handleGitCommit(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
1032
|
+
case 'git_commit':
|
|
1033
|
+
handleGitCommit(msg, state.workDir, send);
|
|
1179
1034
|
break;
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
const m = msg;
|
|
1183
|
-
handleGitStash(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
1035
|
+
case 'git_stash':
|
|
1036
|
+
handleGitStash(msg, state.workDir, send);
|
|
1184
1037
|
break;
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
const m = msg;
|
|
1188
|
-
handleGitPull(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
1038
|
+
case 'git_pull':
|
|
1039
|
+
handleGitPull(msg, state.workDir, send);
|
|
1189
1040
|
break;
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
const m = msg;
|
|
1193
|
-
handleGitPush(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
1041
|
+
case 'git_push':
|
|
1042
|
+
handleGitPush(msg, state.workDir, send);
|
|
1194
1043
|
break;
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
const m = msg;
|
|
1198
|
-
handleGitDeleteFile(m, resolveWorkDir(msg, state, runtime), tag(send, m.conversationId));
|
|
1044
|
+
case 'git_delete_file':
|
|
1045
|
+
handleGitDeleteFile(msg, state.workDir, send);
|
|
1199
1046
|
break;
|
|
1200
|
-
}
|
|
1201
1047
|
case 'list_recaps':
|
|
1202
1048
|
handleListRecaps();
|
|
1203
1049
|
break;
|
|
@@ -1293,7 +1139,7 @@ function handleServerMessage(msg) {
|
|
|
1293
1139
|
// ── Terminal handlers ──────────────────────────────────────────────
|
|
1294
1140
|
case 'terminal_open':
|
|
1295
1141
|
if (terminalManager)
|
|
1296
|
-
terminalManager.open(msg.cols, msg.rows,
|
|
1142
|
+
terminalManager.open(msg.cols, msg.rows, state.workDir);
|
|
1297
1143
|
break;
|
|
1298
1144
|
case 'terminal_input':
|
|
1299
1145
|
if (terminalManager)
|
|
@@ -1558,30 +1404,21 @@ async function handleBrainChatDeleteSession(msg) {
|
|
|
1558
1404
|
send({ type: 'brainchat_error', message: String(err) });
|
|
1559
1405
|
}
|
|
1560
1406
|
}
|
|
1561
|
-
async function handleListSessions(
|
|
1562
|
-
const reply = (m) => {
|
|
1563
|
-
if (conversationId)
|
|
1564
|
-
m.conversationId = conversationId;
|
|
1565
|
-
send(m);
|
|
1566
|
-
};
|
|
1407
|
+
async function handleListSessions() {
|
|
1567
1408
|
try {
|
|
1568
1409
|
if (!runtime) {
|
|
1569
|
-
|
|
1410
|
+
send({ type: 'sessions_list', sessions: [], workDir: state.workDir });
|
|
1570
1411
|
return;
|
|
1571
1412
|
}
|
|
1572
1413
|
// PR4-D: runtime.listHistory returns BackendSessionInfo rows; map to the
|
|
1573
1414
|
// flat wire shape connection.ts has always sent ({sessionId, title, ...}).
|
|
1574
|
-
const rows = await runtime.listHistory(
|
|
1415
|
+
const rows = await runtime.listHistory(state.workDir);
|
|
1575
1416
|
const sessions = rows.map(r => ({
|
|
1576
1417
|
sessionId: r.session.backendSessionId,
|
|
1577
1418
|
title: r.title,
|
|
1578
1419
|
customTitle: r.customTitle,
|
|
1579
1420
|
preview: r.preview,
|
|
1580
1421
|
lastModified: r.lastModified,
|
|
1581
|
-
// PROJECT PATH: tag each session with the workdir its JSONL lives under,
|
|
1582
|
-
// so the web client can `set_conversation_workdir` before resuming
|
|
1583
|
-
// (otherwise per-conv resume reads the wrong directory). See sidebar.js.
|
|
1584
|
-
projectPath: resolvedWorkDir,
|
|
1585
1422
|
}));
|
|
1586
1423
|
const metaMap = loadAllSessionMetadata();
|
|
1587
1424
|
const enriched = sessions.map(s => ({
|
|
@@ -1589,7 +1426,7 @@ async function handleListSessions(resolvedWorkDir, conversationId) {
|
|
|
1589
1426
|
...metaMap.get(s.sessionId),
|
|
1590
1427
|
}));
|
|
1591
1428
|
// Always merge recap/briefing/devops chat sessions from BrainData directory — they live under a
|
|
1592
|
-
// different Claude project folder, so listHistory(
|
|
1429
|
+
// different Claude project folder, so listHistory(state.workDir) misses them.
|
|
1593
1430
|
// These sessions should be visible regardless of the current workDir.
|
|
1594
1431
|
const brainRows = await runtime.listHistory(BRAIN_DATA_DIR);
|
|
1595
1432
|
const brainSessions = brainRows.map(r => ({
|
|
@@ -1598,7 +1435,6 @@ async function handleListSessions(resolvedWorkDir, conversationId) {
|
|
|
1598
1435
|
customTitle: r.customTitle,
|
|
1599
1436
|
preview: r.preview,
|
|
1600
1437
|
lastModified: r.lastModified,
|
|
1601
|
-
projectPath: BRAIN_DATA_DIR,
|
|
1602
1438
|
}));
|
|
1603
1439
|
const existingIds = new Set(enriched.map(s => s.sessionId));
|
|
1604
1440
|
for (const bs of brainSessions) {
|
|
@@ -1610,12 +1446,12 @@ async function handleListSessions(resolvedWorkDir, conversationId) {
|
|
|
1610
1446
|
}
|
|
1611
1447
|
}
|
|
1612
1448
|
const cs = getSessionCacheStats();
|
|
1613
|
-
console.log(`[AgentLink] → sessions_list (${enriched.length} sessions for ${
|
|
1614
|
-
|
|
1449
|
+
console.log(`[AgentLink] → sessions_list (${enriched.length} sessions for ${state.workDir}) cache size=${cs.size} hits=${cs.hits} misses=${cs.misses} stale=${cs.staleMisses} ttlSkips=${cs.ttlSkips} evictions=${cs.evictions}`);
|
|
1450
|
+
send({ type: 'sessions_list', sessions: enriched, workDir: state.workDir });
|
|
1615
1451
|
}
|
|
1616
1452
|
catch (err) {
|
|
1617
1453
|
console.error(`[AgentLink] listSessions failed:`, err);
|
|
1618
|
-
|
|
1454
|
+
send({ type: 'sessions_list', sessions: [], workDir: state.workDir });
|
|
1619
1455
|
}
|
|
1620
1456
|
}
|
|
1621
1457
|
async function handleListRecentSessions(msg) {
|
|
@@ -1636,11 +1472,9 @@ async function handleListRecentSessions(msg) {
|
|
|
1636
1472
|
// echoing the searchId.
|
|
1637
1473
|
const searchControllers = new Map();
|
|
1638
1474
|
function handleSearchSessions(msg) {
|
|
1639
|
-
const { searchId, query, limit
|
|
1475
|
+
const { searchId, query, limit } = msg;
|
|
1640
1476
|
if (!searchId)
|
|
1641
1477
|
return;
|
|
1642
|
-
const tagged = tag(send, conversationId);
|
|
1643
|
-
const resolvedWorkDir = resolveWorkDir(msg, state, runtime);
|
|
1644
1478
|
// Abort any prior in-flight search (only one at a time).
|
|
1645
1479
|
for (const [id, ctrl] of searchControllers) {
|
|
1646
1480
|
ctrl.abort();
|
|
@@ -1656,11 +1490,11 @@ function handleSearchSessions(msg) {
|
|
|
1656
1490
|
return;
|
|
1657
1491
|
}
|
|
1658
1492
|
try {
|
|
1659
|
-
const result = searchSessions(
|
|
1493
|
+
const result = searchSessions(state.workDir, query, { limit, signal: controller.signal });
|
|
1660
1494
|
if (controller.signal.aborted)
|
|
1661
1495
|
return;
|
|
1662
1496
|
console.log(`[AgentLink] → session_search_results (q="${query}" hits=${result.results.reduce((s, r) => s + r.hits.length, 0)} scanned=${result.scannedFiles} matched=${result.matchedFiles} ${result.durationMs}ms)`);
|
|
1663
|
-
|
|
1497
|
+
send({
|
|
1664
1498
|
type: 'session_search_results',
|
|
1665
1499
|
searchId,
|
|
1666
1500
|
...result,
|
|
@@ -1668,7 +1502,7 @@ function handleSearchSessions(msg) {
|
|
|
1668
1502
|
}
|
|
1669
1503
|
catch (err) {
|
|
1670
1504
|
console.error('[AgentLink] searchSessions failed:', err);
|
|
1671
|
-
|
|
1505
|
+
send({
|
|
1672
1506
|
type: 'session_search_error',
|
|
1673
1507
|
searchId,
|
|
1674
1508
|
message: String(err),
|
|
@@ -1686,19 +1520,18 @@ function handleCancelSearch(msg) {
|
|
|
1686
1520
|
searchControllers.delete(msg.searchId);
|
|
1687
1521
|
}
|
|
1688
1522
|
}
|
|
1689
|
-
async function handleDeleteSession(sessionId
|
|
1690
|
-
const reply = tag(send, conversationId);
|
|
1523
|
+
async function handleDeleteSession(sessionId) {
|
|
1691
1524
|
// Evict any idle conversation holding this session; block if busy
|
|
1692
1525
|
if (evictByClaudeSessionId(sessionId)) {
|
|
1693
|
-
|
|
1526
|
+
send({ type: 'error', message: 'Cannot delete a session while it is processing.' });
|
|
1694
1527
|
return;
|
|
1695
1528
|
}
|
|
1696
1529
|
if (!runtime) {
|
|
1697
|
-
|
|
1530
|
+
send({ type: 'error', message: 'Session not found or could not be deleted.' });
|
|
1698
1531
|
return;
|
|
1699
1532
|
}
|
|
1700
1533
|
// Try current workDir first; if not found, check if it's a recap/briefing/devops/project session in BrainData
|
|
1701
|
-
let deleted = await runtime.deleteHistorySession(
|
|
1534
|
+
let deleted = await runtime.deleteHistorySession(state.workDir, sessionId);
|
|
1702
1535
|
if (!deleted) {
|
|
1703
1536
|
const meta = loadSessionMetadata(sessionId);
|
|
1704
1537
|
if (meta.recapId || meta.briefingDate || meta.devopsEntityType || meta.projectName || meta.icmId) {
|
|
@@ -1707,20 +1540,19 @@ async function handleDeleteSession(sessionId, resolvedWorkDir, conversationId) {
|
|
|
1707
1540
|
}
|
|
1708
1541
|
if (deleted) {
|
|
1709
1542
|
deleteSessionMetadata(sessionId);
|
|
1710
|
-
|
|
1543
|
+
send({ type: 'session_deleted', sessionId });
|
|
1711
1544
|
}
|
|
1712
1545
|
else {
|
|
1713
|
-
|
|
1546
|
+
send({ type: 'error', message: 'Session not found or could not be deleted.' });
|
|
1714
1547
|
}
|
|
1715
1548
|
}
|
|
1716
|
-
async function handleRenameSession(sessionId, newTitle
|
|
1717
|
-
const reply = tag(send, conversationId);
|
|
1549
|
+
async function handleRenameSession(sessionId, newTitle) {
|
|
1718
1550
|
if (!runtime) {
|
|
1719
|
-
|
|
1551
|
+
send({ type: 'error', message: 'Session not found or could not be renamed.' });
|
|
1720
1552
|
return;
|
|
1721
1553
|
}
|
|
1722
1554
|
// Try current workDir first; if not found, check if it's a recap/briefing/devops/project session in BrainData
|
|
1723
|
-
let renamed = await runtime.renameHistorySession(
|
|
1555
|
+
let renamed = await runtime.renameHistorySession(state.workDir, sessionId, newTitle);
|
|
1724
1556
|
if (!renamed) {
|
|
1725
1557
|
const meta = loadSessionMetadata(sessionId);
|
|
1726
1558
|
if (meta.recapId || meta.briefingDate || meta.devopsEntityType || meta.projectName || meta.icmId) {
|
|
@@ -1728,25 +1560,25 @@ async function handleRenameSession(sessionId, newTitle, resolvedWorkDir, convers
|
|
|
1728
1560
|
}
|
|
1729
1561
|
}
|
|
1730
1562
|
if (renamed) {
|
|
1731
|
-
|
|
1563
|
+
send({ type: 'session_renamed', sessionId, newTitle });
|
|
1732
1564
|
}
|
|
1733
1565
|
else {
|
|
1734
1566
|
// Session file may not exist on disk yet (e.g. IcM/project chat still streaming first response).
|
|
1735
1567
|
// Retry once after a short delay; if it still fails, send an error so the UI can recover.
|
|
1736
1568
|
setTimeout(() => {
|
|
1737
1569
|
(async () => {
|
|
1738
|
-
const retried = (await runtime.renameHistorySession(
|
|
1570
|
+
const retried = (await runtime.renameHistorySession(state.workDir, sessionId, newTitle))
|
|
1739
1571
|
|| (await runtime.renameHistorySession(BRAIN_DATA_DIR, sessionId, newTitle));
|
|
1740
1572
|
if (retried) {
|
|
1741
|
-
|
|
1573
|
+
send({ type: 'session_renamed', sessionId, newTitle });
|
|
1742
1574
|
}
|
|
1743
1575
|
else {
|
|
1744
1576
|
console.warn(`[AgentLink] rename_session: session ${sessionId.slice(0, 8)} not found after retry`);
|
|
1745
|
-
|
|
1577
|
+
send({ type: 'error', message: 'Session not found or could not be renamed.' });
|
|
1746
1578
|
}
|
|
1747
1579
|
})().catch((err) => {
|
|
1748
1580
|
console.error('[AgentLink] rename_session retry failed:', err);
|
|
1749
|
-
|
|
1581
|
+
send({ type: 'error', message: 'Session not found or could not be renamed.' });
|
|
1750
1582
|
});
|
|
1751
1583
|
}, 1500);
|
|
1752
1584
|
}
|