teleportation-cli 1.1.4 → 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/config-loader.mjs +88 -34
- package/.claude/hooks/permission_request.mjs +392 -82
- package/.claude/hooks/post_tool_use.mjs +90 -0
- package/.claude/hooks/pre_tool_use.mjs +247 -305
- package/.claude/hooks/session-register.mjs +94 -105
- 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/README.md +7 -0
- package/lib/auth/api-key.js +12 -0
- package/lib/auth/token-refresh.js +286 -0
- 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/response-classifier.js +15 -1
- 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 +1235 -0
- package/lib/daemon/teleportation-daemon.js +770 -25
- 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 +11 -5
- package/teleportation-cli.cjs +632 -451
- package/.claude/hooks/heartbeat.mjs +0 -396
- package/lib/daemon/agentic-executor.js +0 -803
- package/lib/daemon/pid-manager.js +0 -160
|
@@ -31,10 +31,10 @@ 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
|
-
* @returns {Promise<boolean>} - True if registered successfully
|
|
37
|
+
* @returns {Promise<boolean|object>} - True if registered successfully, or error object if orphan
|
|
38
38
|
*/
|
|
39
39
|
export async function ensureSessionRegistered(session_id, cwd, config) {
|
|
40
40
|
const RELAY_API_URL = config.relayApiUrl || '';
|
|
@@ -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,119 +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;
|
|
168
|
+
const { appendFile, mkdir } = await import('fs/promises');
|
|
169
|
+
const sessionLogPath = join(dirname(registrationMarker), 'session-events.log');
|
|
140
170
|
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
const { spawn } = await import('child_process');
|
|
144
|
-
const heartbeatPath = join(__dirname, 'heartbeat.mjs');
|
|
171
|
+
// Ensure directory exists
|
|
172
|
+
await mkdir(dirname(sessionLogPath), { recursive: true, mode: 0o700 }).catch(() => {});
|
|
145
173
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
+
};
|
|
150
183
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
let pidData;
|
|
156
|
-
try {
|
|
157
|
-
pidData = JSON.parse(pidContent);
|
|
158
|
-
} catch {
|
|
159
|
-
// Fallback for old format (plain PID number)
|
|
160
|
-
pidData = { pid: parseInt(pidContent.trim(), 10) };
|
|
161
|
-
}
|
|
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);
|
|
162
188
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
}
|
|
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`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
199
194
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
}
|
|
195
|
+
// Session file already created above (before registration)
|
|
196
|
+
// This ensures daemon can start tracking immediately
|
|
197
|
+
return true;
|
|
198
|
+
} else if (response.status === 403) {
|
|
199
|
+
const data = await response.json().catch(() => ({}));
|
|
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)');
|
|
218
208
|
}
|
|
219
|
-
|
|
220
|
-
const heartbeat = spawn('node', [heartbeatPath, session_id], {
|
|
221
|
-
detached: true,
|
|
222
|
-
stdio: 'ignore',
|
|
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
|
-
}
|
|
209
|
+
} catch {}
|
|
236
210
|
}
|
|
237
|
-
|
|
238
|
-
|
|
211
|
+
return { success: false, error: 'orphan_api_key', message: data.message };
|
|
212
|
+
}
|
|
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);
|
|
239
220
|
if (env.DEBUG) {
|
|
240
|
-
console.error('[SessionRegister]
|
|
221
|
+
console.error('[SessionRegister] Cleaned up session file (registration failed)');
|
|
241
222
|
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return true;
|
|
223
|
+
} catch {}
|
|
245
224
|
}
|
|
246
225
|
} catch (e) {
|
|
247
|
-
// 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
|
+
|
|
248
237
|
if (env.DEBUG) {
|
|
249
238
|
console.error('[SessionRegister] Failed to register session:', e.message);
|
|
250
239
|
}
|
|
@@ -291,6 +280,7 @@ export async function updateSessionMetadata(session_id, cwd, config) {
|
|
|
291
280
|
if (metadataModule && metadataModule.extractSessionMetadata && cwd) {
|
|
292
281
|
const extracted = await metadataModule.extractSessionMetadata(cwd);
|
|
293
282
|
extracted.session_id = session_id;
|
|
283
|
+
extracted.claude_session_id = session_id; // Critical: preserve claude_session_id for task resumption
|
|
294
284
|
metadata = extracted;
|
|
295
285
|
}
|
|
296
286
|
} catch (e) {
|
|
@@ -330,4 +320,3 @@ export async function updateSessionMetadata(session_id, cwd, config) {
|
|
|
330
320
|
|
|
331
321
|
return false;
|
|
332
322
|
}
|
|
333
|
-
|
|
@@ -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
|
|