teleportation-cli 1.1.5 → 1.2.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/.claude/hooks/permission_request.mjs +326 -59
- package/.claude/hooks/post_tool_use.mjs +90 -0
- package/.claude/hooks/pre_tool_use.mjs +212 -293
- package/.claude/hooks/session-register.mjs +89 -104
- package/.claude/hooks/session_end.mjs +41 -42
- package/.claude/hooks/session_start.mjs +45 -60
- package/.claude/hooks/stop.mjs +752 -99
- package/.claude/hooks/user_prompt_submit.mjs +26 -3
- package/lib/cli/daemon-commands.js +1 -1
- package/lib/cli/teleport-commands.js +469 -0
- package/lib/daemon/daemon-v2.js +104 -0
- package/lib/daemon/lifecycle.js +56 -171
- package/lib/daemon/services/index.js +3 -0
- package/lib/daemon/services/polling-service.js +173 -0
- package/lib/daemon/services/queue-service.js +318 -0
- package/lib/daemon/services/session-service.js +115 -0
- package/lib/daemon/state.js +35 -0
- package/lib/daemon/task-executor-v2.js +413 -0
- package/lib/daemon/task-executor.js +270 -96
- package/lib/daemon/teleportation-daemon.js +709 -126
- package/lib/daemon/timeline-analyzer.js +215 -0
- package/lib/daemon/transcript-ingestion.js +696 -0
- package/lib/daemon/utils.js +91 -0
- package/lib/install/installer.js +184 -20
- package/lib/install/uhr-installer.js +136 -0
- package/lib/remote/providers/base-provider.js +46 -0
- package/lib/remote/providers/daytona-provider.js +58 -0
- package/lib/remote/providers/provider-factory.js +90 -19
- package/lib/remote/providers/sprites-provider.js +711 -0
- package/lib/teleport/exporters/claude-exporter.js +302 -0
- package/lib/teleport/exporters/gemini-exporter.js +307 -0
- package/lib/teleport/exporters/index.js +93 -0
- package/lib/teleport/exporters/interface.js +153 -0
- package/lib/teleport/fork-tracker.js +415 -0
- package/lib/teleport/git-committer.js +337 -0
- package/lib/teleport/index.js +48 -0
- package/lib/teleport/manager.js +620 -0
- package/lib/teleport/session-capture.js +282 -0
- package/package.json +9 -5
- package/teleportation-cli.cjs +488 -453
- package/.claude/hooks/heartbeat.mjs +0 -396
- package/lib/daemon/pid-manager.js +0 -183
|
@@ -31,7 +31,7 @@ async function loadVersionInfo() {
|
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Register a session with the relay API if not already registered
|
|
34
|
-
* @param {string} session_id - Session ID
|
|
34
|
+
* @param {string} session_id - Session ID (also Claude session ID)
|
|
35
35
|
* @param {string} cwd - Current working directory
|
|
36
36
|
* @param {object} config - Config object with relayApiUrl and relayApiKey
|
|
37
37
|
* @returns {Promise<boolean|object>} - True if registered successfully, or error object if orphan
|
|
@@ -66,7 +66,7 @@ export async function ensureSessionRegistered(session_id, cwd, config) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// Extract enhanced session metadata
|
|
69
|
-
let metadata = { cwd };
|
|
69
|
+
let metadata = { cwd, claude_session_id: session_id };
|
|
70
70
|
try {
|
|
71
71
|
// Try to load metadata extraction module
|
|
72
72
|
const possiblePaths = [
|
|
@@ -88,6 +88,7 @@ export async function ensureSessionRegistered(session_id, cwd, config) {
|
|
|
88
88
|
if (metadataModule && metadataModule.extractSessionMetadata && cwd) {
|
|
89
89
|
const extracted = await metadataModule.extractSessionMetadata(cwd);
|
|
90
90
|
extracted.session_id = session_id;
|
|
91
|
+
extracted.claude_session_id = session_id; // Include Claude session ID for autonomous task resumption
|
|
91
92
|
metadata = extracted;
|
|
92
93
|
}
|
|
93
94
|
} catch (e) {
|
|
@@ -108,6 +109,36 @@ export async function ensureSessionRegistered(session_id, cwd, config) {
|
|
|
108
109
|
// Version info not available - old installation
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
// Mark cloud sessions (set by cloud provider bootstrap scripts)
|
|
113
|
+
if (env.TELEPORTATION_IS_CLOUD === 'true') {
|
|
114
|
+
metadata.is_cloud = true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// PRD-0002: Create session file BEFORE relay registration
|
|
118
|
+
// This prevents race condition where registration succeeds but hook crashes before creating file
|
|
119
|
+
// If registration fails, we cleanup the session file
|
|
120
|
+
const sessionFile = join(tmpdir(), `teleportation-session-${session_id}.json`);
|
|
121
|
+
const claudePid = process.ppid || process.pid; // Use parent (Claude CLI), fallback to current process
|
|
122
|
+
let sessionFileCreated = false;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
await writeFile(sessionFile, JSON.stringify({
|
|
126
|
+
session_id,
|
|
127
|
+
claude_pid: claudePid,
|
|
128
|
+
cwd: cwd || process.cwd(),
|
|
129
|
+
registered_at: Date.now(),
|
|
130
|
+
meta: metadata
|
|
131
|
+
}), { mode: 0o600 });
|
|
132
|
+
sessionFileCreated = true;
|
|
133
|
+
|
|
134
|
+
if (env.DEBUG) {
|
|
135
|
+
console.error(`[SessionRegister] Session file created (Claude PID: ${claudePid})`);
|
|
136
|
+
}
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.error('[SessionRegister] CRITICAL: Failed to create session file:', e.message);
|
|
139
|
+
// Continue with registration anyway - daemon won't track but session will exist
|
|
140
|
+
}
|
|
141
|
+
|
|
111
142
|
try {
|
|
112
143
|
if (env.DEBUG) {
|
|
113
144
|
console.error(`[SessionRegister] Calling ${RELAY_API_URL}/api/sessions/register`);
|
|
@@ -132,124 +163,77 @@ export async function ensureSessionRegistered(session_id, cwd, config) {
|
|
|
132
163
|
// Ignore marker write failures - registration still succeeded
|
|
133
164
|
}
|
|
134
165
|
|
|
135
|
-
//
|
|
166
|
+
// Write to shared session log for daemon discovery
|
|
136
167
|
try {
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
const maxFailures = config.session?.heartbeat?.maxFailures || 3;
|
|
140
|
-
|
|
141
|
-
// Heartbeats are mandatory for session liveness tracking
|
|
142
|
-
if (RELAY_API_URL && RELAY_API_KEY) {
|
|
143
|
-
const { spawn } = await import('child_process');
|
|
144
|
-
const heartbeatPath = join(__dirname, 'heartbeat.mjs');
|
|
168
|
+
const { appendFile, mkdir } = await import('fs/promises');
|
|
169
|
+
const sessionLogPath = join(dirname(registrationMarker), 'session-events.log');
|
|
145
170
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const { readFile, unlink } = await import('fs/promises');
|
|
149
|
-
const pidFile = join(tmpdir(), `teleportation-heartbeat-${session_id}.pid`);
|
|
171
|
+
// Ensure directory exists
|
|
172
|
+
await mkdir(dirname(sessionLogPath), { recursive: true, mode: 0o700 }).catch(() => {});
|
|
150
173
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
pidData = { pid: parseInt(pidContent.trim(), 10) };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const pid = pidData.pid;
|
|
164
|
-
if (pid && !isNaN(pid)) {
|
|
165
|
-
// Check if the process is actually running
|
|
166
|
-
try {
|
|
167
|
-
process.kill(pid, 0); // Signal 0 checks existence without killing
|
|
168
|
-
// Process is running - don't start another heartbeat
|
|
169
|
-
shouldStartHeartbeat = false;
|
|
170
|
-
if (env.DEBUG) {
|
|
171
|
-
console.error(`[SessionRegister] Heartbeat already running (PID: ${pid})`);
|
|
172
|
-
}
|
|
173
|
-
} catch (killError) {
|
|
174
|
-
if (killError.code === 'ESRCH') {
|
|
175
|
-
// Process is dead - clean up stale PID file
|
|
176
|
-
try {
|
|
177
|
-
await unlink(pidFile);
|
|
178
|
-
if (env.DEBUG) {
|
|
179
|
-
console.error(`[SessionRegister] Cleaned up stale heartbeat PID file (PID: ${pid})`);
|
|
180
|
-
}
|
|
181
|
-
} catch (unlinkError) {
|
|
182
|
-
if (env.DEBUG) {
|
|
183
|
-
console.error(`[SessionRegister] Failed to unlink stale PID file: ${unlinkError.message}`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
// Will start new heartbeat
|
|
187
|
-
} else {
|
|
188
|
-
// Permission error - assume process exists, don't start duplicate
|
|
189
|
-
shouldStartHeartbeat = false;
|
|
190
|
-
if (env.DEBUG) {
|
|
191
|
-
console.error(`[SessionRegister] Cannot check heartbeat (permission denied), assuming running (PID: ${pid})`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
} catch (e) {
|
|
197
|
-
// PID file doesn't exist, start heartbeat
|
|
198
|
-
}
|
|
174
|
+
const logEvent = {
|
|
175
|
+
type: 'register',
|
|
176
|
+
session_id: session_id,
|
|
177
|
+
claude_session_id: session_id,
|
|
178
|
+
pid: process.pid,
|
|
179
|
+
cwd: cwd || process.cwd(),
|
|
180
|
+
meta: metadata,
|
|
181
|
+
timestamp: Date.now()
|
|
182
|
+
};
|
|
199
183
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const { execSync } = await import('child_process');
|
|
205
|
-
// Use pkill with -f flag to match full command line (session_id argument)
|
|
206
|
-
// Returns 1 if no processes matched, so we ignore the exit code
|
|
207
|
-
execSync(`pkill -f "heartbeat.mjs ${session_id}" 2>/dev/null || true`, { stdio: 'ignore' });
|
|
208
|
-
if (env.DEBUG) {
|
|
209
|
-
console.error(`[SessionRegister] Cleaned up any orphan heartbeats for ${session_id}`);
|
|
210
|
-
}
|
|
211
|
-
// Small delay to let orphan processes terminate
|
|
212
|
-
await new Promise(r => setTimeout(r, 100));
|
|
213
|
-
} catch (pkillError) {
|
|
214
|
-
// Ignore pkill errors - process might not exist
|
|
215
|
-
if (env.DEBUG) {
|
|
216
|
-
console.error(`[SessionRegister] pkill cleanup failed: ${pkillError.message}`);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
184
|
+
await appendFile(sessionLogPath, JSON.stringify(logEvent) + '\n', 'utf8');
|
|
185
|
+
} catch (e) {
|
|
186
|
+
// Log all failures - session recovery depends on this
|
|
187
|
+
console.error('[SessionRegister] WARN: Failed to write session log:', e.message);
|
|
219
188
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
env: {
|
|
224
|
-
...process.env,
|
|
225
|
-
SESSION_ID: session_id,
|
|
226
|
-
RELAY_API_URL,
|
|
227
|
-
RELAY_API_KEY,
|
|
228
|
-
HEARTBEAT_INTERVAL: String(heartbeatInterval),
|
|
229
|
-
START_DELAY: String(startDelay),
|
|
230
|
-
MAX_FAILURES: String(maxFailures),
|
|
231
|
-
TELEPORTATION_DEBUG: process.env.TELEPORTATION_DEBUG || 'false'
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
heartbeat.unref();
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
} catch (error) {
|
|
238
|
-
// Don't fail registration if heartbeat spawn fails
|
|
239
|
-
if (env.DEBUG) {
|
|
240
|
-
console.error('[SessionRegister] Failed to spawn heartbeat:', error.message);
|
|
189
|
+
// Critical errors should be visible
|
|
190
|
+
if (e.code === 'EACCES' || e.code === 'ENOSPC') {
|
|
191
|
+
console.error(`[SessionRegister] CRITICAL: ${e.code} - Session recovery may fail after daemon restart`);
|
|
241
192
|
}
|
|
242
193
|
}
|
|
243
194
|
|
|
195
|
+
// Session file already created above (before registration)
|
|
196
|
+
// This ensures daemon can start tracking immediately
|
|
244
197
|
return true;
|
|
245
198
|
} else if (response.status === 403) {
|
|
246
199
|
const data = await response.json().catch(() => ({}));
|
|
247
200
|
if (data.error === 'orphan_api_key') {
|
|
201
|
+
// Cleanup session file on orphan key error
|
|
202
|
+
if (sessionFileCreated) {
|
|
203
|
+
try {
|
|
204
|
+
const { unlink } = await import('fs/promises');
|
|
205
|
+
await unlink(sessionFile);
|
|
206
|
+
if (env.DEBUG) {
|
|
207
|
+
console.error('[SessionRegister] Cleaned up session file (orphan API key)');
|
|
208
|
+
}
|
|
209
|
+
} catch {}
|
|
210
|
+
}
|
|
248
211
|
return { success: false, error: 'orphan_api_key', message: data.message };
|
|
249
212
|
}
|
|
250
213
|
}
|
|
214
|
+
|
|
215
|
+
// Registration failed - cleanup session file to prevent orphan
|
|
216
|
+
if (sessionFileCreated) {
|
|
217
|
+
try {
|
|
218
|
+
const { unlink } = await import('fs/promises');
|
|
219
|
+
await unlink(sessionFile);
|
|
220
|
+
if (env.DEBUG) {
|
|
221
|
+
console.error('[SessionRegister] Cleaned up session file (registration failed)');
|
|
222
|
+
}
|
|
223
|
+
} catch {}
|
|
224
|
+
}
|
|
251
225
|
} catch (e) {
|
|
252
|
-
// Registration failed -
|
|
226
|
+
// Registration failed - cleanup session file
|
|
227
|
+
if (sessionFileCreated) {
|
|
228
|
+
try {
|
|
229
|
+
const { unlink } = await import('fs/promises');
|
|
230
|
+
await unlink(sessionFile);
|
|
231
|
+
if (env.DEBUG) {
|
|
232
|
+
console.error('[SessionRegister] Cleaned up session file (exception)');
|
|
233
|
+
}
|
|
234
|
+
} catch {}
|
|
235
|
+
}
|
|
236
|
+
|
|
253
237
|
if (env.DEBUG) {
|
|
254
238
|
console.error('[SessionRegister] Failed to register session:', e.message);
|
|
255
239
|
}
|
|
@@ -296,6 +280,7 @@ export async function updateSessionMetadata(session_id, cwd, config) {
|
|
|
296
280
|
if (metadataModule && metadataModule.extractSessionMetadata && cwd) {
|
|
297
281
|
const extracted = await metadataModule.extractSessionMetadata(cwd);
|
|
298
282
|
extracted.session_id = session_id;
|
|
283
|
+
extracted.claude_session_id = session_id; // Critical: preserve claude_session_id for task resumption
|
|
299
284
|
metadata = extracted;
|
|
300
285
|
}
|
|
301
286
|
} catch (e) {
|
|
@@ -40,6 +40,10 @@ const readStdin = () => new Promise((resolve, reject) => {
|
|
|
40
40
|
const DAEMON_PORT = config.daemonPort || env.TELEPORTATION_DAEMON_PORT || '3050';
|
|
41
41
|
const DAEMON_ENABLED = config.daemonEnabled !== false && env.TELEPORTATION_DAEMON_ENABLED !== 'false';
|
|
42
42
|
|
|
43
|
+
// Detect message source
|
|
44
|
+
const isAutonomousTask = env.TELEPORTATION_TASK_MODE === 'true' || !!env.TELEPORTATION_PARENT_SESSION_ID;
|
|
45
|
+
const source = isAutonomousTask ? 'autonomous_task' : 'cli_interactive';
|
|
46
|
+
|
|
43
47
|
const updateSessionDaemonState = async (updates) => {
|
|
44
48
|
if (!session_id || !RELAY_API_URL || !RELAY_API_KEY) return;
|
|
45
49
|
try {
|
|
@@ -54,55 +58,38 @@ const readStdin = () => new Promise((resolve, reject) => {
|
|
|
54
58
|
} catch {}
|
|
55
59
|
};
|
|
56
60
|
|
|
57
|
-
//
|
|
61
|
+
// NOTE: Heartbeat processes are no longer spawned per-session.
|
|
62
|
+
// The daemon handles heartbeats inline (PRD-0025 migration).
|
|
63
|
+
|
|
58
64
|
if (session_id) {
|
|
65
|
+
// Write unregister event to shared session log
|
|
59
66
|
try {
|
|
60
|
-
const
|
|
61
|
-
const
|
|
67
|
+
const { appendFile, mkdir } = await import('fs/promises');
|
|
68
|
+
const { homedir } = await import('os');
|
|
69
|
+
const { dirname } = await import('path');
|
|
70
|
+
const sessionLogPath = join(homedir(), '.teleportation', 'session-events.log');
|
|
62
71
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
pidData = JSON.parse(pidContent);
|
|
67
|
-
} catch {
|
|
68
|
-
// Fallback for old format (plain PID number)
|
|
69
|
-
pidData = { pid: parseInt(pidContent.trim(), 10) };
|
|
70
|
-
}
|
|
72
|
+
// Ensure directory exists
|
|
73
|
+
await mkdir(dirname(sessionLogPath), { recursive: true, mode: 0o700 }).catch(() => {});
|
|
71
74
|
|
|
72
|
-
const
|
|
75
|
+
const logEvent = {
|
|
76
|
+
type: 'unregister',
|
|
77
|
+
session_id: session_id,
|
|
78
|
+
claude_session_id: session_id,
|
|
79
|
+
pid: process.pid,
|
|
80
|
+
cwd: process.cwd(),
|
|
81
|
+
timestamp: Date.now()
|
|
82
|
+
};
|
|
73
83
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
} else {
|
|
79
|
-
try {
|
|
80
|
-
// Verify process exists before killing
|
|
81
|
-
process.kill(pid, 0); // Signal 0 checks existence without killing
|
|
82
|
-
|
|
83
|
-
// Process exists, safe to kill
|
|
84
|
-
process.kill(pid, 'SIGTERM');
|
|
85
|
-
console.log(`[SessionEnd] Killed heartbeat process (PID: ${pid})`);
|
|
86
|
-
} catch (killError) {
|
|
87
|
-
if (killError.code === 'ESRCH') {
|
|
88
|
-
// Process already dead, that's okay
|
|
89
|
-
console.log(`[SessionEnd] Heartbeat process already terminated (PID: ${pid})`);
|
|
90
|
-
} else {
|
|
91
|
-
// Permission error or other issue
|
|
92
|
-
console.error(`[SessionEnd] Failed to kill heartbeat:`, killError.message);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
84
|
+
await appendFile(sessionLogPath, JSON.stringify(logEvent) + '\n', 'utf8');
|
|
85
|
+
} catch (e) {
|
|
86
|
+
// Log all failures - session recovery depends on this
|
|
87
|
+
console.error('[SessionEnd] WARN: Failed to write session log:', e.message);
|
|
97
88
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
} catch (unlinkError) {
|
|
102
|
-
// Ignore errors - file might not exist
|
|
89
|
+
// Critical errors should be visible
|
|
90
|
+
if (e.code === 'EACCES' || e.code === 'ENOSPC') {
|
|
91
|
+
console.error(`[SessionEnd] CRITICAL: ${e.code} - Session recovery may fail after daemon restart`);
|
|
103
92
|
}
|
|
104
|
-
} catch (error) {
|
|
105
|
-
// Ignore errors reading PID file - heartbeat might not have been started
|
|
106
93
|
}
|
|
107
94
|
|
|
108
95
|
// Delete registration marker file
|
|
@@ -113,6 +100,17 @@ const readStdin = () => new Promise((resolve, reject) => {
|
|
|
113
100
|
// Ignore errors - marker might not exist
|
|
114
101
|
}
|
|
115
102
|
|
|
103
|
+
// Delete daemon session file (prevents /tmp accumulation)
|
|
104
|
+
try {
|
|
105
|
+
const daemonSessionFile = join(tmpdir(), `teleportation-session-${session_id}.json`);
|
|
106
|
+
await unlink(daemonSessionFile);
|
|
107
|
+
if (env.DEBUG) {
|
|
108
|
+
console.error(`[SessionEnd] Deleted daemon session file: ${daemonSessionFile}`);
|
|
109
|
+
}
|
|
110
|
+
} catch (e) {
|
|
111
|
+
// Ignore errors - file might not exist
|
|
112
|
+
}
|
|
113
|
+
|
|
116
114
|
// Clean up model tracking files
|
|
117
115
|
try {
|
|
118
116
|
const lastModelFile = join(tmpdir(), `teleportation-last-model-${session_id}.txt`);
|
|
@@ -215,6 +213,7 @@ const readStdin = () => new Promise((resolve, reject) => {
|
|
|
215
213
|
body: JSON.stringify({
|
|
216
214
|
session_id,
|
|
217
215
|
type: 'session_end',
|
|
216
|
+
source,
|
|
218
217
|
data: {
|
|
219
218
|
reason: 'normal_exit',
|
|
220
219
|
timestamp: Date.now()
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { stdin, exit, env } from 'node:process';
|
|
4
|
-
import { spawn } from 'child_process';
|
|
5
4
|
import { fileURLToPath } from 'url';
|
|
6
5
|
import { dirname, join } from 'path';
|
|
7
6
|
import { homedir } from 'os';
|
|
8
7
|
import { existsSync, statSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { agentStart, agentStatus } from '@derivativelabs/agent-process';
|
|
9
9
|
|
|
10
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
11
|
const __dirname = dirname(__filename);
|
|
@@ -166,14 +166,22 @@ function updateSessionMarker(sessionId) {
|
|
|
166
166
|
// Auto-start daemon if enabled
|
|
167
167
|
if (DAEMON_ENABLED && session_id && RELAY_API_URL && RELAY_API_KEY) {
|
|
168
168
|
try {
|
|
169
|
-
// Check if daemon is already running
|
|
169
|
+
// Check if daemon is already running using agent-runtime
|
|
170
170
|
const daemonUrl = `http://127.0.0.1:${DAEMON_PORT}`;
|
|
171
171
|
let daemonRunning = false;
|
|
172
172
|
|
|
173
173
|
try {
|
|
174
|
-
const
|
|
175
|
-
daemonRunning =
|
|
176
|
-
|
|
174
|
+
const status = await agentStatus('teleportation-daemon');
|
|
175
|
+
daemonRunning = status.state === 'online' || status.running === true;
|
|
176
|
+
if (env.DEBUG && daemonRunning) {
|
|
177
|
+
console.error(`[SessionStart] Daemon already running (PID: ${status.pid})`);
|
|
178
|
+
}
|
|
179
|
+
} catch (statusError) {
|
|
180
|
+
// Agent not installed or not running - need to start it
|
|
181
|
+
if (env.DEBUG) {
|
|
182
|
+
console.error(`[SessionStart] Daemon status check: ${statusError.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
177
185
|
|
|
178
186
|
// Start daemon if not running
|
|
179
187
|
if (!daemonRunning) {
|
|
@@ -209,63 +217,40 @@ function updateSessionMarker(sessionId) {
|
|
|
209
217
|
return exit(0);
|
|
210
218
|
}
|
|
211
219
|
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
220
|
+
// Start daemon using agent-runtime
|
|
221
|
+
// This handles retries, health checks, and platform-native service installation
|
|
222
|
+
try {
|
|
223
|
+
const handle = await agentStart({
|
|
224
|
+
name: 'teleportation-daemon',
|
|
225
|
+
script: daemonScript,
|
|
226
|
+
port: parseInt(DAEMON_PORT),
|
|
227
|
+
interpreter: process.execPath, // Use bun
|
|
228
|
+
env: {
|
|
229
|
+
...process.env,
|
|
230
|
+
TELEPORTATION_DAEMON: 'true',
|
|
231
|
+
RELAY_API_URL,
|
|
232
|
+
RELAY_API_KEY,
|
|
233
|
+
TELEPORTATION_DAEMON_PORT: DAEMON_PORT,
|
|
234
|
+
// Disable idle timeout - daemon should stay running indefinitely
|
|
235
|
+
DAEMON_IDLE_TIMEOUT_MS: process.env.DAEMON_IDLE_TIMEOUT_MS || '0'
|
|
236
|
+
},
|
|
237
|
+
restart: true, // Auto-restart on crash
|
|
238
|
+
maxRestarts: 10,
|
|
239
|
+
restartBackoff: 1000,
|
|
240
|
+
maxMemory: '500M',
|
|
241
|
+
});
|
|
230
242
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
daemonStarted = true;
|
|
240
|
-
if (env.DEBUG) {
|
|
241
|
-
console.error('[SessionStart] Daemon started successfully');
|
|
242
|
-
}
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
} catch (healthError) {
|
|
246
|
-
if (env.DEBUG) {
|
|
247
|
-
console.error(`[SessionStart] Health check failed: ${healthError.message}`);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
retries--;
|
|
252
|
-
if (retries === 0 && !daemonStarted) {
|
|
253
|
-
if (env.DEBUG) {
|
|
254
|
-
console.error('[SessionStart] Failed to start daemon after 3 attempts');
|
|
255
|
-
}
|
|
256
|
-
// Set flag to disable daemon for this session
|
|
257
|
-
process.env.TELEPORTATION_DAEMON_DISABLED = 'true';
|
|
258
|
-
}
|
|
259
|
-
} catch (spawnError) {
|
|
260
|
-
retries--;
|
|
261
|
-
if (env.DEBUG) {
|
|
262
|
-
console.error(`[SessionStart] Daemon spawn error (${3 - retries}/3):`, spawnError.message);
|
|
263
|
-
}
|
|
264
|
-
if (retries > 0) {
|
|
265
|
-
// Exponential backoff between retries
|
|
266
|
-
await new Promise(r => setTimeout(r, 1000 * (4 - retries)));
|
|
267
|
-
}
|
|
243
|
+
if (env.DEBUG) {
|
|
244
|
+
console.error(`[SessionStart] Daemon started successfully (PID: ${handle.pid}, Platform: ${handle.platform})`);
|
|
245
|
+
}
|
|
246
|
+
} catch (startError) {
|
|
247
|
+
console.error(`\n⚠️ Teleportation daemon failed to start: ${startError.message}`);
|
|
248
|
+
console.error(' Sessions may appear INACTIVE. Run: teleportation doctor\n');
|
|
249
|
+
if (env.DEBUG) {
|
|
250
|
+
console.error(`[SessionStart] Stack: ${startError.stack}`);
|
|
268
251
|
}
|
|
252
|
+
// Set flag to disable daemon for this session
|
|
253
|
+
process.env.TELEPORTATION_DAEMON_DISABLED = 'true';
|
|
269
254
|
}
|
|
270
255
|
}
|
|
271
256
|
|