@dsiloed/silo-link 1.3.0 → 1.5.0
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/api/dsiloed-client.d.ts +30 -0
- package/dist/api/dsiloed-client.d.ts.map +1 -1
- package/dist/api/dsiloed-client.js +66 -0
- package/dist/api/dsiloed-client.js.map +1 -1
- package/dist/cable/subscription-manager.d.ts +7 -3
- package/dist/cable/subscription-manager.d.ts.map +1 -1
- package/dist/cable/subscription-manager.js +36 -11
- package/dist/cable/subscription-manager.js.map +1 -1
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +118 -27
- package/dist/cli/commands.js.map +1 -1
- package/dist/config/config-manager.d.ts +4 -4
- package/dist/config/config-manager.d.ts.map +1 -1
- package/dist/config/config-manager.js +30 -15
- package/dist/config/config-manager.js.map +1 -1
- package/dist/core/agent-launcher.d.ts +30 -1
- package/dist/core/agent-launcher.d.ts.map +1 -1
- package/dist/core/base-tmux-launcher.d.ts +106 -0
- package/dist/core/base-tmux-launcher.d.ts.map +1 -0
- package/dist/core/base-tmux-launcher.js +506 -0
- package/dist/core/base-tmux-launcher.js.map +1 -0
- package/dist/core/bridge.d.ts +19 -2
- package/dist/core/bridge.d.ts.map +1 -1
- package/dist/core/bridge.js +155 -37
- package/dist/core/bridge.js.map +1 -1
- package/dist/core/claude-launcher.d.ts +28 -100
- package/dist/core/claude-launcher.d.ts.map +1 -1
- package/dist/core/claude-launcher.js +164 -487
- package/dist/core/claude-launcher.js.map +1 -1
- package/dist/core/gemini-launcher.d.ts +37 -27
- package/dist/core/gemini-launcher.d.ts.map +1 -1
- package/dist/core/gemini-launcher.js +135 -38
- package/dist/core/gemini-launcher.js.map +1 -1
- package/dist/core/launcher-factory.d.ts.map +1 -1
- package/dist/core/launcher-factory.js +1 -2
- package/dist/core/launcher-factory.js.map +1 -1
- package/dist/core/openai-launcher.d.ts +14 -17
- package/dist/core/openai-launcher.d.ts.map +1 -1
- package/dist/core/openai-launcher.js +23 -44
- package/dist/core/openai-launcher.js.map +1 -1
- package/dist/core/session-manager.d.ts +3 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +11 -0
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/workspace-manager.d.ts.map +1 -1
- package/dist/core/workspace-manager.js +53 -16
- package/dist/core/workspace-manager.js.map +1 -1
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +19 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/register-tools.d.ts +2 -0
- package/dist/mcp/tools/register-tools.d.ts.map +1 -1
- package/dist/mcp/tools/register-tools.js +174 -16
- package/dist/mcp/tools/register-tools.js.map +1 -1
- package/dist/types/index.d.ts +11 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import jwt from 'jsonwebtoken';
|
|
3
|
+
import { BaseTmuxLauncher } from './base-tmux-launcher.js';
|
|
2
4
|
/**
|
|
3
5
|
* Claude Code implementation of the AgentLauncher interface.
|
|
4
6
|
*
|
|
@@ -9,226 +11,170 @@ import { spawn } from 'node:child_process';
|
|
|
9
11
|
* - Store Claude session ID in DSiloed agent_session metadata
|
|
10
12
|
* - Kill all child processes on SiloLink shutdown
|
|
11
13
|
*/
|
|
12
|
-
export class ClaudeLauncher {
|
|
13
|
-
config;
|
|
14
|
-
sessionManager;
|
|
15
|
-
dsiloedClient;
|
|
16
|
-
workspaceManager;
|
|
14
|
+
export class ClaudeLauncher extends BaseTmuxLauncher {
|
|
17
15
|
childProcess = null;
|
|
18
|
-
tmuxSessions = new Map(); // tmuxName -> conversationId
|
|
19
|
-
tmuxSessionWorkspaces = new Map(); // tmuxName -> sessionId (for worktree cleanup)
|
|
20
|
-
tmuxMonitorTimer = null;
|
|
21
|
-
idleCheckTimer = null;
|
|
22
|
-
launchingConversations = new Set();
|
|
23
|
-
messageQueue = null;
|
|
24
|
-
enabled;
|
|
25
|
-
lastNudgeAt = 0;
|
|
26
|
-
nudgeCount = 0; // Consecutive nudges without activity change
|
|
27
|
-
lastActivitySnapshot = new Map(); // sessionId -> lastActivity timestamp
|
|
28
|
-
// Last Claude Code session ID (for --resume)
|
|
29
|
-
lastClaudeSessionId = null;
|
|
30
|
-
// Message that triggered the current launch (included in prompt for context)
|
|
31
|
-
pendingTriggerMessage = null;
|
|
32
|
-
// Workspace config for the next launch (set by control channel or API)
|
|
33
|
-
pendingWorkspaceConfig = null;
|
|
34
|
-
// Override CWD for next launch (used when re-launching into an existing worktree)
|
|
35
|
-
pendingWorkspaceCwd = null;
|
|
36
16
|
constructor(config, sessionManager, dsiloedClient, messageQueue, workspaceManager) {
|
|
37
|
-
|
|
38
|
-
this.sessionManager = sessionManager;
|
|
39
|
-
this.dsiloedClient = dsiloedClient || null;
|
|
40
|
-
this.messageQueue = messageQueue || null;
|
|
41
|
-
this.workspaceManager = workspaceManager || null;
|
|
42
|
-
this.enabled = !!(config.claude_command && config.claude_working_directory);
|
|
17
|
+
super(config, sessionManager, dsiloedClient, messageQueue, workspaceManager);
|
|
43
18
|
}
|
|
44
|
-
|
|
45
|
-
|
|
19
|
+
// --- Abstract implementation ---
|
|
20
|
+
getCommand() {
|
|
21
|
+
return this.config.claude_command || 'claude';
|
|
46
22
|
}
|
|
47
|
-
|
|
48
|
-
return
|
|
23
|
+
getArgs() {
|
|
24
|
+
return ['--dangerously-skip-permissions'];
|
|
49
25
|
}
|
|
50
|
-
|
|
51
|
-
return
|
|
26
|
+
getReadyIndicator() {
|
|
27
|
+
return ['❯'];
|
|
52
28
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
29
|
+
/** Wait 5s before checking — Claude Code shows ❯ in its welcome screen before it's ready */
|
|
30
|
+
getMinReadyWaitMs() { return 5000; }
|
|
31
|
+
/** Wait 3s after ready for npx mcp-remote servers to finish connecting */
|
|
32
|
+
getPostReadyDelayMs() { return 3000; }
|
|
33
|
+
formatPrompt(prompt) {
|
|
34
|
+
return prompt;
|
|
59
35
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
this.idleCheckTimer = null;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Launch a new Claude Code session. Uses --resume if a previous session ID is available.
|
|
78
|
-
* This is the internal launch method — use launchAgent() for the AgentLauncher interface.
|
|
79
|
-
*/
|
|
80
|
-
async launchInternal(reason = 'manual', conversationId) {
|
|
81
|
-
if (!this.enabled) {
|
|
82
|
-
console.log('[ClaudeLauncher] Not configured — set claude_command and claude_working_directory in config');
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
const convKey = conversationId || 0;
|
|
86
|
-
if (this.launchingConversations.has(convKey)) {
|
|
87
|
-
console.log(`[ClaudeLauncher] Already launching for conversation ${convKey}, skipping`);
|
|
88
|
-
return false;
|
|
36
|
+
async setupMcpConfig(cwd, toolAllowlist) {
|
|
37
|
+
// Generate a JWT for the dsiloed MCP server
|
|
38
|
+
const token = jwt.sign({
|
|
39
|
+
sub: this.config.user_sub,
|
|
40
|
+
iat: Math.floor(Date.now() / 1000),
|
|
41
|
+
tenant_enterprise_identifier: this.config.tenant_id,
|
|
42
|
+
}, this.config.shared_key, { algorithm: 'HS256', expiresIn: '24h' });
|
|
43
|
+
// Build the dsiloed MCP URL pointing to the same server SiloLink connects to
|
|
44
|
+
let dsiloedUrl = `${this.config.host}/api/v1/mcp?categories=data,admin,conversation,utility`;
|
|
45
|
+
if (toolAllowlist && toolAllowlist.length > 0) {
|
|
46
|
+
const url = new URL(`${this.config.host}/api/v1/mcp`);
|
|
47
|
+
url.searchParams.set('tools', toolAllowlist.join(','));
|
|
48
|
+
dsiloedUrl = url.toString();
|
|
89
49
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
50
|
+
// Register MCP servers via `claude mcp add` (project scope for the target CWD).
|
|
51
|
+
// This is the official API — works across Claude Code versions and doesn't
|
|
52
|
+
// require interactive approval when launched with --dangerously-skip-permissions.
|
|
53
|
+
const servers = [
|
|
54
|
+
{
|
|
55
|
+
name: 'silolink',
|
|
56
|
+
args: ['--', 'npx', 'mcp-remote', `http://localhost:${this.config.mcp_port}/mcp`],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'silolink_server',
|
|
60
|
+
args: [
|
|
61
|
+
'--', 'npx', 'mcp-remote', dsiloedUrl,
|
|
62
|
+
'--header', `Authorization: Bearer ${token}`,
|
|
63
|
+
'--header', `Tenant-Id: ${this.config.tenant_id}`,
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
for (const server of servers) {
|
|
68
|
+
try {
|
|
69
|
+
execFileSync('claude', ['mcp', 'add', '-s', 'user', server.name, ...server.args], {
|
|
70
|
+
cwd,
|
|
71
|
+
stdio: 'pipe',
|
|
72
|
+
timeout: 10000,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Server may already exist at user or project scope — remove both and re-add at user scope
|
|
104
77
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
78
|
+
try {
|
|
79
|
+
execFileSync('claude', ['mcp', 'remove', '-s', 'user', server.name], { cwd, stdio: 'pipe', timeout: 5000 });
|
|
80
|
+
}
|
|
81
|
+
catch { /* may not exist */ }
|
|
82
|
+
try {
|
|
83
|
+
execFileSync('claude', ['mcp', 'remove', '-s', 'project', server.name], { cwd, stdio: 'pipe', timeout: 5000 });
|
|
84
|
+
}
|
|
85
|
+
catch { /* may not exist */ }
|
|
86
|
+
execFileSync('claude', ['mcp', 'add', '-s', 'user', server.name, ...server.args], {
|
|
87
|
+
cwd,
|
|
88
|
+
stdio: 'pipe',
|
|
89
|
+
timeout: 10000,
|
|
90
|
+
});
|
|
108
91
|
}
|
|
109
92
|
catch (err) {
|
|
110
|
-
console.error(`[ClaudeLauncher] Failed to
|
|
111
|
-
// Fall back to normal cwd
|
|
112
|
-
workspaceSessionId = null;
|
|
113
|
-
workspaceBranch = null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
// Use existing worktree CWD if set (re-launching into a linked workspace)
|
|
117
|
-
const existingWorkspaceCwd = this.pendingWorkspaceCwd;
|
|
118
|
-
this.pendingWorkspaceCwd = null;
|
|
119
|
-
if (existingWorkspaceCwd && !workspaceSessionId) {
|
|
120
|
-
cwd = existingWorkspaceCwd;
|
|
121
|
-
console.log(`[ClaudeLauncher] Using existing worktree at ${cwd}`);
|
|
122
|
-
}
|
|
123
|
-
let prompt;
|
|
124
|
-
// Build workspace context string for inclusion in prompts
|
|
125
|
-
const workspaceContext = (workspaceSessionId || existingWorkspaceCwd)
|
|
126
|
-
? `\n\nWORKSPACE: You are working in an isolated git worktree at "${cwd}". This is separate from the main repo — your changes won't affect other sessions. Use remote_workspace_claim to claim files before editing, and remote_workspace_check to see what other sessions are working on.`
|
|
127
|
-
: '';
|
|
128
|
-
// Try to resume previous session if we have an ID
|
|
129
|
-
if (this.lastClaudeSessionId && (reason === 'process-exit' || reason === 'inbound-message')) {
|
|
130
|
-
args.push('--resume', this.lastClaudeSessionId);
|
|
131
|
-
console.log(`[ClaudeLauncher] Resuming session: ${this.lastClaudeSessionId}`);
|
|
132
|
-
prompt = 'Continue polling SiloLink for messages. Check memory for context if needed.' + workspaceContext;
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
if (reason === 'process-exit' || reason === 'inbound-message') {
|
|
136
|
-
const registerParams = conversationId
|
|
137
|
-
? `{ "conversation_id": ${conversationId} }`
|
|
138
|
-
: `{}`;
|
|
139
|
-
const triggerLine = this.pendingTriggerMessage
|
|
140
|
-
? `\n\nThe user's latest message (which triggered this session restart): "${this.pendingTriggerMessage}"\nAddress this message AFTER loading context. Do NOT ask the user to repeat themselves.`
|
|
141
|
-
: '';
|
|
142
|
-
this.pendingTriggerMessage = null;
|
|
143
|
-
prompt = [
|
|
144
|
-
`Call mcp__silolink__remote_register with these exact parameters: ${registerParams}. After registering, call remote_load_context() to load the prior conversation history. Review all returned messages to understand what was discussed. Then send a remote_notify telling the user you are continuing where the last session left off. Then enter the poll loop using remote_poll. See CLAUDE.md for the poll loop pattern.`,
|
|
145
|
-
'',
|
|
146
|
-
'Context: This is a new session replacing a previous one that ended.',
|
|
147
|
-
'Before entering the poll loop:',
|
|
148
|
-
'1. Call remote_load_context() and review the conversation history',
|
|
149
|
-
'2. Check your memory files for additional context on recent work',
|
|
150
|
-
'3. Send a remote_notify summarizing what you loaded and confirming you are ready',
|
|
151
|
-
'4. Process the user\'s pending message (if any) before entering the poll loop',
|
|
152
|
-
'',
|
|
153
|
-
'CRITICAL: The user is NOT watching the terminal. You MUST send a remote_notify progress update after EVERY major step (file edits, commits, merges, deploys, errors). Do not go silent while working.',
|
|
154
|
-
triggerLine,
|
|
155
|
-
workspaceContext,
|
|
156
|
-
].join('\n');
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
prompt = (this.config.claude_session_prompt ||
|
|
160
|
-
'Register with SiloLink as "portablemind" and enter the poll loop.') + workspaceContext;
|
|
93
|
+
console.error(`[ClaudeLauncher] Failed to register MCP server '${server.name}': ${err}`);
|
|
161
94
|
}
|
|
162
95
|
}
|
|
163
|
-
console.log(`[ClaudeLauncher] Launching Claude Code via tmux (reason: ${reason})`);
|
|
164
|
-
console.log(` Command: ${command} ${args.join(' ')}`);
|
|
165
|
-
console.log(` Working directory: ${cwd}`);
|
|
166
|
-
if (workspaceBranch)
|
|
167
|
-
console.log(` Workspace branch: ${workspaceBranch}`);
|
|
168
|
-
console.log(` Prompt: ${prompt.substring(0, 100)}...`);
|
|
169
|
-
const tmuxSession = `silolink-claude-${Date.now()}`;
|
|
170
|
-
// Create a new tmux session running Claude Code in a real TTY
|
|
171
|
-
const claudeCmd = `cd ${cwd} && ${command} ${args.join(' ')}`;
|
|
172
|
-
const tmuxCreate = spawn('tmux', [
|
|
173
|
-
'new-session', '-d', '-s', tmuxSession, '-x', '200', '-y', '50', claudeCmd,
|
|
174
|
-
], {
|
|
175
|
-
stdio: 'ignore',
|
|
176
|
-
detached: true,
|
|
177
|
-
env: {
|
|
178
|
-
...process.env,
|
|
179
|
-
SILOLINK_MCP_URL: `http://localhost:${this.config.mcp_port}/mcp`,
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
tmuxCreate.unref();
|
|
183
|
-
// Wait for tmux session to start, then send the prompt
|
|
184
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
185
|
-
// Check if tmux session exists
|
|
186
|
-
const checkResult = spawn('tmux', ['has-session', '-t', tmuxSession], { stdio: 'ignore' });
|
|
187
|
-
const sessionExists = await new Promise(resolve => {
|
|
188
|
-
checkResult.on('exit', code => resolve(code === 0));
|
|
189
|
-
});
|
|
190
|
-
if (!sessionExists) {
|
|
191
|
-
console.error(`[ClaudeLauncher] tmux session ${tmuxSession} failed to start`);
|
|
192
|
-
// Clean up worktree if we created one
|
|
193
|
-
if (workspaceSessionId && this.workspaceManager) {
|
|
194
|
-
this.workspaceManager.removeWorktree(workspaceSessionId);
|
|
195
|
-
}
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
console.log(`[ClaudeLauncher] tmux session "${tmuxSession}" created`);
|
|
199
|
-
this.tmuxSessions.set(tmuxSession, conversationId || 0);
|
|
200
|
-
if (workspaceSessionId) {
|
|
201
|
-
this.tmuxSessionWorkspaces.set(tmuxSession, workspaceSessionId);
|
|
202
|
-
}
|
|
203
|
-
// Send the prompt to Claude via tmux send-keys after MCP servers initialize
|
|
204
|
-
const sessionToPrompt = tmuxSession;
|
|
205
|
-
const promptToSend = prompt;
|
|
206
|
-
setTimeout(() => {
|
|
207
|
-
const escapedPrompt = promptToSend.replace(/"/g, '\\"').replace(/\n/g, ' ');
|
|
208
|
-
const sendKeys = spawn('tmux', ['send-keys', '-t', sessionToPrompt, escapedPrompt], { stdio: 'pipe' });
|
|
209
|
-
sendKeys.on('exit', (code) => {
|
|
210
|
-
if (code === 0) {
|
|
211
|
-
console.log(`[ClaudeLauncher] Sent prompt to tmux session "${sessionToPrompt}" (${promptToSend.length} chars)`);
|
|
212
|
-
// Send Enter separately after a short delay to avoid bracketed paste swallowing it
|
|
213
|
-
setTimeout(() => {
|
|
214
|
-
spawn('tmux', ['send-keys', '-t', sessionToPrompt, 'Enter'], { stdio: 'pipe' });
|
|
215
|
-
}, 500);
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
console.error(`[ClaudeLauncher] Failed to send prompt to tmux session "${sessionToPrompt}" (exit code: ${code})`);
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
sendKeys.on('error', (err) => {
|
|
222
|
-
console.error(`[ClaudeLauncher] Error sending prompt to "${sessionToPrompt}": ${err.message}`);
|
|
223
|
-
});
|
|
224
|
-
}, 8000); // Wait 8s for MCP to initialize
|
|
225
|
-
// Monitor the tmux session for exit
|
|
226
|
-
this.monitorTmuxSession(tmuxSession);
|
|
227
|
-
return true;
|
|
228
96
|
}
|
|
229
|
-
|
|
230
|
-
|
|
97
|
+
console.log(`[ClaudeLauncher] Registered MCP servers for ${cwd} (host: ${this.config.host})`);
|
|
98
|
+
}
|
|
99
|
+
checkEnabled() {
|
|
100
|
+
return !!(this.config.claude_command && this.config.projects_path);
|
|
101
|
+
}
|
|
102
|
+
getWorkingDirectory() {
|
|
103
|
+
return this.config.projects_path || process.cwd();
|
|
104
|
+
}
|
|
105
|
+
isAutoRespawnConfigured() {
|
|
106
|
+
return !!this.config.claude_auto_respawn;
|
|
107
|
+
}
|
|
108
|
+
getIdleTimeoutMs() {
|
|
109
|
+
return this.config.claude_idle_timeout_ms || 30000;
|
|
110
|
+
}
|
|
111
|
+
getSessionPrompt() {
|
|
112
|
+
return this.config.claude_session_prompt;
|
|
113
|
+
}
|
|
114
|
+
getAgentEnv() {
|
|
115
|
+
return {
|
|
116
|
+
SILOLINK_MCP_URL: `http://localhost:${this.config.mcp_port}/mcp`,
|
|
117
|
+
SILOLINK_AGENT_PROVIDER: 'claude',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
getTmuxPrefix() {
|
|
121
|
+
return 'silolink-claude';
|
|
122
|
+
}
|
|
123
|
+
getLogTag() {
|
|
124
|
+
return '[ClaudeLauncher]';
|
|
125
|
+
}
|
|
126
|
+
getCostMetadataKey() {
|
|
127
|
+
return 'claude_code_cost_usd';
|
|
128
|
+
}
|
|
129
|
+
buildLaunchPrompt(reason, conversationId, agentContext, workspaceContext) {
|
|
130
|
+
const extraArgs = [];
|
|
131
|
+
if (this.lastSessionId && (reason === 'process-exit' || reason === 'inbound-message')) {
|
|
132
|
+
extraArgs.push('--resume', this.lastSessionId);
|
|
133
|
+
return {
|
|
134
|
+
prompt: 'Continue polling SiloLink for messages. Check memory for context if needed.' + workspaceContext,
|
|
135
|
+
extraArgs,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (reason === 'process-exit' || reason === 'inbound-message') {
|
|
139
|
+
const agentIdParam = this.pendingLlmAgentId
|
|
140
|
+
? `, "llm_agent_id": ${this.pendingLlmAgentId}`
|
|
141
|
+
: '';
|
|
142
|
+
const registerParams = conversationId
|
|
143
|
+
? `{ "conversation_id": ${conversationId}${agentIdParam} }`
|
|
144
|
+
: `{${agentIdParam ? agentIdParam.substring(2) : ''}}`;
|
|
145
|
+
this.pendingLlmAgentId = null;
|
|
146
|
+
const triggerLine = this.pendingTriggerMessage
|
|
147
|
+
? `\n\nThe user's latest message (which triggered this session restart): "${this.pendingTriggerMessage}"\nAddress this message AFTER loading context. Do NOT ask the user to repeat themselves.`
|
|
148
|
+
: '';
|
|
149
|
+
this.pendingTriggerMessage = null;
|
|
150
|
+
return {
|
|
151
|
+
prompt: [
|
|
152
|
+
`Call mcp__silolink__remote_register with these exact parameters: ${registerParams}. After registering, call remote_load_context() to load the prior conversation history. Review all returned messages to understand what was discussed. Then send a remote_notify telling the user you are continuing where the last session left off. Then enter the poll loop using remote_poll. See CLAUDE.md for the poll loop pattern.`,
|
|
153
|
+
'',
|
|
154
|
+
'Context: This is a new session replacing a previous one that ended.',
|
|
155
|
+
'Before entering the poll loop:',
|
|
156
|
+
'1. Call remote_load_context() and review the conversation history',
|
|
157
|
+
'2. Check your memory files for additional context on recent work',
|
|
158
|
+
'3. Send a remote_notify summarizing what you loaded and confirming you are ready',
|
|
159
|
+
'4. Process the user\'s pending message (if any) before entering the poll loop',
|
|
160
|
+
'',
|
|
161
|
+
'CRITICAL: The user is NOT watching the terminal. You MUST send a remote_notify progress update after EVERY major step (file edits, commits, merges, deploys, errors). Do not go silent while working.',
|
|
162
|
+
triggerLine,
|
|
163
|
+
agentContext,
|
|
164
|
+
workspaceContext,
|
|
165
|
+
].join('\n'),
|
|
166
|
+
extraArgs,
|
|
167
|
+
};
|
|
231
168
|
}
|
|
169
|
+
return {
|
|
170
|
+
prompt: (this.config.claude_session_prompt ||
|
|
171
|
+
'Register with SiloLink as "portablemind" and enter the poll loop.') + agentContext + workspaceContext,
|
|
172
|
+
extraArgs,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// --- Claude-specific additions ---
|
|
176
|
+
getLastClaudeSessionId() {
|
|
177
|
+
return this.lastSessionId;
|
|
232
178
|
}
|
|
233
179
|
/**
|
|
234
180
|
* Store the Claude resume ID in the DSiloed agent session metadata.
|
|
@@ -248,64 +194,21 @@ export class ClaudeLauncher {
|
|
|
248
194
|
}
|
|
249
195
|
}
|
|
250
196
|
/**
|
|
251
|
-
*
|
|
252
|
-
* The worktree will be created when launch() is called.
|
|
253
|
-
*/
|
|
254
|
-
setWorkspaceConfig(config) {
|
|
255
|
-
this.pendingWorkspaceConfig = config;
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Called when an inbound message arrives and no active session can handle it.
|
|
259
|
-
* @param triggerMessage - Content of the message that triggered the launch (included in prompt)
|
|
260
|
-
*/
|
|
261
|
-
async launchOnMessage(conversationId, triggerMessage) {
|
|
262
|
-
if (!this.enabled)
|
|
263
|
-
return false;
|
|
264
|
-
// Check if there's already a session registered for this conversation
|
|
265
|
-
if (conversationId) {
|
|
266
|
-
const existingSession = this.sessionManager.getByConversationId(conversationId);
|
|
267
|
-
if (existingSession) {
|
|
268
|
-
console.log(`[ClaudeLauncher] Session already exists for conversation ${conversationId} — skipping`);
|
|
269
|
-
return false;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
// Store trigger message so the launch prompt can include it
|
|
273
|
-
this.pendingTriggerMessage = triggerMessage || null;
|
|
274
|
-
// If a workspace is linked to this conversation, set it as pending so
|
|
275
|
-
// launchInternal creates the session in the correct worktree directory.
|
|
276
|
-
if (conversationId && this.workspaceManager) {
|
|
277
|
-
const workspace = this.workspaceManager.getByConversationId(conversationId);
|
|
278
|
-
if (workspace) {
|
|
279
|
-
console.log(`[ClaudeLauncher] Found linked workspace for conv=${conversationId}: ${workspace.branch}`);
|
|
280
|
-
// Don't create a new worktree — just set CWD to existing one.
|
|
281
|
-
// We skip setWorkspaceConfig to avoid re-creating the worktree.
|
|
282
|
-
// Instead, override the working directory directly for this launch.
|
|
283
|
-
this.pendingWorkspaceCwd = workspace.worktreePath;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
return this.launchInternal('inbound-message', conversationId);
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* Check for idle sessions — nudge if process alive, respawn if dead.
|
|
290
|
-
* If a session has been nudged multiple times without responding and has
|
|
291
|
-
* pending messages, kill and relaunch it (hung session recovery).
|
|
197
|
+
* Extended idle checking with Claude-specific hung session recovery.
|
|
292
198
|
*/
|
|
293
199
|
async checkIdle() {
|
|
294
200
|
const sessions = this.sessionManager.getAll();
|
|
295
|
-
const idleTimeout = this.
|
|
296
|
-
const maxNudges = 10;
|
|
201
|
+
const idleTimeout = this.getIdleTimeoutMs();
|
|
202
|
+
const maxNudges = 10;
|
|
297
203
|
const now = Date.now();
|
|
298
204
|
if (sessions.length === 0)
|
|
299
205
|
return;
|
|
300
|
-
|
|
301
|
-
// process the launch prompt and initial messages before intervening.
|
|
302
|
-
const startupGracePeriod = 90000; // 90 seconds
|
|
206
|
+
const startupGracePeriod = 90000;
|
|
303
207
|
const allPastStartup = sessions.every(s => now - s.createdAt.getTime() > startupGracePeriod);
|
|
304
208
|
if (!allPastStartup)
|
|
305
209
|
return;
|
|
306
210
|
const allIdle = sessions.every(s => now - s.lastActivity.getTime() > idleTimeout);
|
|
307
211
|
if (!allIdle) {
|
|
308
|
-
// Activity detected — reset nudge counter and update snapshots
|
|
309
212
|
this.nudgeCount = 0;
|
|
310
213
|
for (const s of sessions) {
|
|
311
214
|
this.lastActivitySnapshot.set(s.sessionId, s.lastActivity.getTime());
|
|
@@ -313,7 +216,6 @@ export class ClaudeLauncher {
|
|
|
313
216
|
return;
|
|
314
217
|
}
|
|
315
218
|
if (this.hasLiveProcess()) {
|
|
316
|
-
// Check if activity changed since last nudge
|
|
317
219
|
const activityChanged = sessions.some(s => {
|
|
318
220
|
const prev = this.lastActivitySnapshot.get(s.sessionId) || 0;
|
|
319
221
|
return s.lastActivity.getTime() > prev;
|
|
@@ -321,15 +223,11 @@ export class ClaudeLauncher {
|
|
|
321
223
|
if (activityChanged) {
|
|
322
224
|
this.nudgeCount = 0;
|
|
323
225
|
}
|
|
324
|
-
// Update snapshots
|
|
325
226
|
for (const s of sessions) {
|
|
326
227
|
this.lastActivitySnapshot.set(s.sessionId, s.lastActivity.getTime());
|
|
327
228
|
}
|
|
328
|
-
// Check if any session has messages that haven't been delivered yet.
|
|
329
|
-
// Messages already polled by Claude (pending ack) don't count — Claude is working on them.
|
|
330
229
|
const hasUndelivered = this.messageQueue && sessions.some(s => this.messageQueue.hasUndeliveredMessages(s.sessionId));
|
|
331
230
|
if (this.nudgeCount >= maxNudges && hasUndelivered) {
|
|
332
|
-
// Session is hung with undelivered messages — kill and relaunch
|
|
333
231
|
console.log(`[ClaudeLauncher] Session hung (${this.nudgeCount} nudges with no response, undelivered messages) — killing and relaunching`);
|
|
334
232
|
const hungSessions = sessions.filter(s => this.messageQueue.hasUndeliveredMessages(s.sessionId));
|
|
335
233
|
for (const s of hungSessions) {
|
|
@@ -338,36 +236,28 @@ export class ClaudeLauncher {
|
|
|
338
236
|
this.messageQueue.clearSession(s.sessionId);
|
|
339
237
|
}
|
|
340
238
|
this.sessionManager.unregister(s.sessionId);
|
|
341
|
-
// Wait for tmux kill to complete before relaunching
|
|
342
239
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
343
240
|
await this.launchInternal('inbound-message', s.conversationId);
|
|
344
241
|
}
|
|
345
242
|
this.nudgeCount = 0;
|
|
346
243
|
return;
|
|
347
244
|
}
|
|
348
|
-
|
|
349
|
-
// AND has undelivered messages waiting. A session doing non-MCP work (file edits, bash
|
|
350
|
-
// commands, etc.) is not a zombie — it's just not using SiloLink tools.
|
|
351
|
-
const zombieTimeout = 10 * 60 * 1000; // 10 minutes
|
|
245
|
+
const zombieTimeout = 10 * 60 * 1000;
|
|
352
246
|
const zombieSessions = sessions.filter(s => now - s.lastActivity.getTime() > zombieTimeout &&
|
|
353
247
|
this.messageQueue?.hasUndeliveredMessages(s.sessionId));
|
|
354
248
|
for (const s of zombieSessions) {
|
|
355
249
|
console.log(`[ClaudeLauncher] Zombie session detected (no MCP activity for ${Math.round((now - s.lastActivity.getTime()) / 60000)}min, has undelivered messages) — killing conv=${s.conversationId}`);
|
|
356
250
|
this.killByConversation(s.conversationId);
|
|
357
|
-
// Clear queue before unregister so messages can be re-buffered on relaunch
|
|
358
251
|
if (this.messageQueue) {
|
|
359
252
|
this.messageQueue.clearSession(s.sessionId);
|
|
360
253
|
}
|
|
361
254
|
this.sessionManager.unregister(s.sessionId);
|
|
362
|
-
// Wait for tmux kill to complete before relaunching
|
|
363
255
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
364
256
|
console.log(`[ClaudeLauncher] Relaunching for conversation ${s.conversationId} after zombie cleanup`);
|
|
365
257
|
this.launchInternal('inbound-message', s.conversationId).catch(err => {
|
|
366
258
|
console.error(`[ClaudeLauncher] Relaunch after zombie kill failed: ${err}`);
|
|
367
259
|
});
|
|
368
260
|
}
|
|
369
|
-
// Only nudge if there are messages waiting — no point nudging an idle session
|
|
370
|
-
// that's already polling with nothing to do
|
|
371
261
|
if (hasUndelivered) {
|
|
372
262
|
this.nudge();
|
|
373
263
|
this.nudgeCount++;
|
|
@@ -375,241 +265,28 @@ export class ClaudeLauncher {
|
|
|
375
265
|
return;
|
|
376
266
|
}
|
|
377
267
|
else if (this.launchingConversations.size === 0) {
|
|
378
|
-
// Process is dead — clean up orphaned sessions instead of auto-respawning.
|
|
379
|
-
// New sessions will be launched on-demand when inbound messages arrive
|
|
380
|
-
// (handled by SubscriptionManager.handleCableMessage → launchOnMessage).
|
|
381
268
|
console.log(`[ClaudeLauncher] Sessions orphaned (process dead) — cleaning up ${sessions.length} session(s). Will relaunch on next inbound message.`);
|
|
382
269
|
for (const s of sessions) {
|
|
383
|
-
if (!this.
|
|
384
|
-
this.
|
|
270
|
+
if (!this.lastSessionId && s.claudeResumeId) {
|
|
271
|
+
this.lastSessionId = s.claudeResumeId;
|
|
385
272
|
}
|
|
386
273
|
this.sessionManager.unregister(s.sessionId);
|
|
387
274
|
}
|
|
388
275
|
this.nudgeCount = 0;
|
|
389
276
|
}
|
|
390
277
|
}
|
|
391
|
-
/**
|
|
392
|
-
* Monitor a tmux session and detect when it exits.
|
|
393
|
-
*/
|
|
394
|
-
monitorTmuxSession(sessionName) {
|
|
395
|
-
// Start monitor if not already running
|
|
396
|
-
if (!this.tmuxMonitorTimer) {
|
|
397
|
-
this.tmuxMonitorTimer = setInterval(() => this.checkTmuxSessions(), 5000);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
checkTmuxSessions() {
|
|
401
|
-
for (const [name] of this.tmuxSessions) {
|
|
402
|
-
const check = spawn('tmux', ['has-session', '-t', name], { stdio: 'ignore' });
|
|
403
|
-
check.on('exit', (code) => {
|
|
404
|
-
if (code !== 0) {
|
|
405
|
-
console.log(`[ClaudeLauncher] tmux session "${name}" ended`);
|
|
406
|
-
// Clean up worktree if this session had one
|
|
407
|
-
const wsSessionId = this.tmuxSessionWorkspaces.get(name);
|
|
408
|
-
if (wsSessionId && this.workspaceManager) {
|
|
409
|
-
console.log(`[ClaudeLauncher] Cleaning up worktree for workspace session ${wsSessionId}`);
|
|
410
|
-
this.workspaceManager.removeWorktree(wsSessionId);
|
|
411
|
-
this.tmuxSessionWorkspaces.delete(name);
|
|
412
|
-
}
|
|
413
|
-
this.tmuxSessions.delete(name);
|
|
414
|
-
if (this.tmuxSessions.size === 0 && this.tmuxMonitorTimer) {
|
|
415
|
-
clearInterval(this.tmuxMonitorTimer);
|
|
416
|
-
this.tmuxMonitorTimer = null;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* Send a message to all running Claude sessions via tmux send-keys.
|
|
424
|
-
*/
|
|
425
|
-
nudge() {
|
|
426
|
-
if (this.tmuxSessions.size === 0)
|
|
427
|
-
return false;
|
|
428
|
-
const now = Date.now();
|
|
429
|
-
// Exponential backoff: 30s, 60s, 120s, 240s, ... (capped at 5 min)
|
|
430
|
-
const baseInterval = 30000;
|
|
431
|
-
const interval = Math.min(baseInterval * Math.pow(2, this.nudgeCount), 5 * 60 * 1000);
|
|
432
|
-
if (now - this.lastNudgeAt < interval)
|
|
433
|
-
return false;
|
|
434
|
-
this.lastNudgeAt = now;
|
|
435
|
-
const message = 'Continue polling SiloLink for messages. Do not stop the poll loop.';
|
|
436
|
-
for (const [name] of this.tmuxSessions) {
|
|
437
|
-
spawn('tmux', ['send-keys', '-t', name, message, 'Enter'], { stdio: 'ignore' });
|
|
438
|
-
}
|
|
439
|
-
console.log(`[ClaudeLauncher] Nudged ${this.tmuxSessions.size} Claude session(s) via tmux (attempt ${this.nudgeCount + 1}, next in ${Math.round(interval / 1000)}s)`);
|
|
440
|
-
return true;
|
|
441
|
-
}
|
|
442
|
-
/**
|
|
443
|
-
* Interrupt the Claude session for a specific conversation.
|
|
444
|
-
* Sends Escape to stop current work, then redirects Claude to check
|
|
445
|
-
* for new messages via remote_poll.
|
|
446
|
-
*/
|
|
447
|
-
async interrupt(conversationId, message) {
|
|
448
|
-
for (const [name, convId] of this.tmuxSessions) {
|
|
449
|
-
if (convId === conversationId) {
|
|
450
|
-
console.log(`[ClaudeLauncher] Interrupting tmux session "${name}" (conversation ${conversationId})`);
|
|
451
|
-
// Send Escape to interrupt current work
|
|
452
|
-
spawn('tmux', ['send-keys', '-t', name, 'Escape'], { stdio: 'ignore' });
|
|
453
|
-
// Wait for Claude to stop
|
|
454
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
455
|
-
// Tell Claude to check for the new message
|
|
456
|
-
const redirect = message
|
|
457
|
-
|| 'The user interrupted you. Check remote_poll for their latest message and follow those instructions instead.';
|
|
458
|
-
const escapedRedirect = redirect.replace(/"/g, '\\"').replace(/\n/g, ' ');
|
|
459
|
-
spawn('tmux', ['send-keys', '-t', name, escapedRedirect], { stdio: 'ignore' });
|
|
460
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
461
|
-
spawn('tmux', ['send-keys', '-t', name, 'Enter'], { stdio: 'ignore' });
|
|
462
|
-
console.log(`[ClaudeLauncher] Interrupt sent to "${name}"`);
|
|
463
|
-
return true;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
console.log(`[ClaudeLauncher] No tmux session found for conversation ${conversationId} to interrupt`);
|
|
467
|
-
return false;
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* Kill the Claude session for a specific conversation.
|
|
471
|
-
*/
|
|
472
|
-
killByConversation(conversationId) {
|
|
473
|
-
for (const [name, convId] of this.tmuxSessions) {
|
|
474
|
-
if (convId === conversationId) {
|
|
475
|
-
console.log(`[ClaudeLauncher] Killing tmux session "${name}" (conversation ${conversationId})`);
|
|
476
|
-
// Clean up worktree
|
|
477
|
-
const wsSessionId = this.tmuxSessionWorkspaces.get(name);
|
|
478
|
-
if (wsSessionId && this.workspaceManager) {
|
|
479
|
-
this.workspaceManager.removeWorktree(wsSessionId);
|
|
480
|
-
this.tmuxSessionWorkspaces.delete(name);
|
|
481
|
-
}
|
|
482
|
-
spawn('tmux', ['kill-session', '-t', name], { stdio: 'ignore' });
|
|
483
|
-
this.tmuxSessions.delete(name);
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
console.log(`[ClaudeLauncher] No tmux session found for conversation ${conversationId}`);
|
|
488
|
-
}
|
|
489
|
-
/**
|
|
490
|
-
* Kill all running Claude Code sessions.
|
|
491
|
-
*/
|
|
492
278
|
kill() {
|
|
493
|
-
|
|
494
|
-
console.log(`[ClaudeLauncher] Killing tmux session "${name}"`);
|
|
495
|
-
// Clean up worktree
|
|
496
|
-
const wsSessionId = this.tmuxSessionWorkspaces.get(name);
|
|
497
|
-
if (wsSessionId && this.workspaceManager) {
|
|
498
|
-
this.workspaceManager.removeWorktree(wsSessionId);
|
|
499
|
-
}
|
|
500
|
-
spawn('tmux', ['kill-session', '-t', name], { stdio: 'ignore' });
|
|
501
|
-
}
|
|
502
|
-
this.tmuxSessions.clear();
|
|
503
|
-
this.tmuxSessionWorkspaces.clear();
|
|
504
|
-
if (this.tmuxMonitorTimer) {
|
|
505
|
-
clearInterval(this.tmuxMonitorTimer);
|
|
506
|
-
this.tmuxMonitorTimer = null;
|
|
507
|
-
}
|
|
279
|
+
super.kill();
|
|
508
280
|
if (this.childProcess) {
|
|
509
281
|
this.childProcess.kill('SIGTERM');
|
|
510
282
|
this.childProcess = null;
|
|
511
283
|
}
|
|
512
284
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
async launch(config) {
|
|
518
|
-
if (config.workspace) {
|
|
519
|
-
this.setWorkspaceConfig(config.workspace);
|
|
520
|
-
}
|
|
521
|
-
if (config.triggerMessage) {
|
|
522
|
-
this.pendingTriggerMessage = config.triggerMessage;
|
|
523
|
-
}
|
|
524
|
-
if (config.sessionPrompt) {
|
|
525
|
-
this.config.claude_session_prompt = config.sessionPrompt;
|
|
526
|
-
}
|
|
527
|
-
const success = await this.launchInternal(config.reason, config.conversationId);
|
|
528
|
-
if (!success)
|
|
529
|
-
return null;
|
|
530
|
-
// Find the process we just created (most recent tmux session)
|
|
531
|
-
const entries = Array.from(this.tmuxSessions.entries());
|
|
532
|
-
const latest = entries[entries.length - 1];
|
|
533
|
-
if (!latest)
|
|
534
|
-
return null;
|
|
535
|
-
const [processId, conversationId] = latest;
|
|
536
|
-
const wsSessionId = this.tmuxSessionWorkspaces.get(processId);
|
|
537
|
-
return {
|
|
538
|
-
processId,
|
|
539
|
-
conversationId,
|
|
540
|
-
workingDirectory: this.config.claude_working_directory || process.cwd(),
|
|
541
|
-
branch: config.workspace?.branch,
|
|
542
|
-
workspaceSessionId: wsSessionId || undefined,
|
|
543
|
-
startedAt: new Date(),
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
/**
|
|
547
|
-
* Resume a previous session by ID.
|
|
548
|
-
*/
|
|
549
|
-
async resume(resumeId, config) {
|
|
550
|
-
this.setLastResumeId(resumeId);
|
|
551
|
-
return this.launch({ ...config, reason: 'process-exit' });
|
|
552
|
-
}
|
|
553
|
-
/**
|
|
554
|
-
* Check if a specific process is still alive.
|
|
555
|
-
*/
|
|
556
|
-
async isAlive(process) {
|
|
557
|
-
return new Promise(resolve => {
|
|
558
|
-
const check = spawn('tmux', ['has-session', '-t', process.processId], { stdio: 'ignore' });
|
|
559
|
-
check.on('exit', code => resolve(code === 0));
|
|
560
|
-
check.on('error', () => resolve(false));
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
/**
|
|
564
|
-
* Terminate a specific agent process.
|
|
565
|
-
*/
|
|
566
|
-
terminate(process) {
|
|
567
|
-
this.killByConversation(process.conversationId);
|
|
568
|
-
}
|
|
569
|
-
/**
|
|
570
|
-
* Terminate all managed agent processes.
|
|
571
|
-
*/
|
|
572
|
-
terminateAll() {
|
|
573
|
-
this.kill();
|
|
574
|
-
}
|
|
575
|
-
/**
|
|
576
|
-
* Get all currently running agent processes.
|
|
577
|
-
*/
|
|
578
|
-
getProcesses() {
|
|
579
|
-
const processes = [];
|
|
580
|
-
for (const [processId, conversationId] of this.tmuxSessions) {
|
|
581
|
-
const wsSessionId = this.tmuxSessionWorkspaces.get(processId);
|
|
582
|
-
processes.push({
|
|
583
|
-
processId,
|
|
584
|
-
conversationId,
|
|
585
|
-
workingDirectory: this.config.claude_working_directory || process.cwd(),
|
|
586
|
-
workspaceSessionId: wsSessionId,
|
|
587
|
-
startedAt: new Date(), // Approximate — we don't track exact start time per tmux session
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
return processes;
|
|
591
|
-
}
|
|
592
|
-
/**
|
|
593
|
-
* Get the process handling a specific conversation.
|
|
594
|
-
*/
|
|
595
|
-
getByConversation(conversationId) {
|
|
596
|
-
for (const [processId, convId] of this.tmuxSessions) {
|
|
597
|
-
if (convId === conversationId) {
|
|
598
|
-
const wsSessionId = this.tmuxSessionWorkspaces.get(processId);
|
|
599
|
-
return {
|
|
600
|
-
processId,
|
|
601
|
-
conversationId,
|
|
602
|
-
workingDirectory: this.config.claude_working_directory || process.cwd(),
|
|
603
|
-
workspaceSessionId: wsSessionId,
|
|
604
|
-
startedAt: new Date(),
|
|
605
|
-
};
|
|
606
|
-
}
|
|
285
|
+
stopIdleMonitor() {
|
|
286
|
+
if (this.idleCheckTimer) {
|
|
287
|
+
clearInterval(this.idleCheckTimer);
|
|
288
|
+
this.idleCheckTimer = null;
|
|
607
289
|
}
|
|
608
|
-
return undefined;
|
|
609
|
-
}
|
|
610
|
-
destroy() {
|
|
611
|
-
this.stopIdleMonitor();
|
|
612
|
-
this.kill();
|
|
613
290
|
}
|
|
614
291
|
}
|
|
615
292
|
//# sourceMappingURL=claude-launcher.js.map
|