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