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
|
@@ -1,803 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agentic Executor
|
|
3
|
-
*
|
|
4
|
-
* Executes agentic tasks using Claude Code's headless mode.
|
|
5
|
-
* Provides an autonomous execution loop that continues until:
|
|
6
|
-
* - Task is complete
|
|
7
|
-
* - Claude asks a question requiring user input
|
|
8
|
-
* - User stops the task
|
|
9
|
-
* - Budget is exhausted
|
|
10
|
-
*
|
|
11
|
-
* Key features:
|
|
12
|
-
* - Uses `claude -p` with `--output-format json` for structured results
|
|
13
|
-
* - Captures `session_id` from JSON result for conversation continuity
|
|
14
|
-
* - Uses `--resume <session-id>` for subsequent turns
|
|
15
|
-
* - Budget control via `--max-budget-usd`
|
|
16
|
-
* - Auto-approval via `--permission-mode bypassPermissions`
|
|
17
|
-
*
|
|
18
|
-
* @module lib/daemon/agentic-executor
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { spawn } from 'child_process';
|
|
22
|
-
import { randomUUID } from 'crypto';
|
|
23
|
-
import { classifyResponse, getConfidenceThreshold } from './response-classifier.js';
|
|
24
|
-
|
|
25
|
-
// Default configuration
|
|
26
|
-
const DEFAULT_BUDGET_USD = 10.0;
|
|
27
|
-
const DEFAULT_TIMEOUT_MS = 600000; // 10 minutes per turn
|
|
28
|
-
const CLAUDE_CLI = process.env.CLAUDE_CLI_PATH || 'claude';
|
|
29
|
-
|
|
30
|
-
// Safety limits to prevent runaway execution
|
|
31
|
-
const MAX_TURNS = 100; // Maximum execution turns before forced stop
|
|
32
|
-
const WAIT_FOR_RESUME_TIMEOUT_MS = 1800000; // 30 minutes max wait for resume
|
|
33
|
-
const POLLING_INTERVAL_MS = 1000; // Polling interval for waitForResume
|
|
34
|
-
const SESSION_CLEANUP_INTERVAL_MS = 300000; // 5 minutes cleanup interval
|
|
35
|
-
const SESSION_MAX_AGE_MS = 3600000; // 1 hour max session age
|
|
36
|
-
const MAX_CONSECUTIVE_FAILURES = 3; // Stop after this many consecutive failures
|
|
37
|
-
const MAX_HISTORY_SIZE = 20; // Maximum history entries to keep per session
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Agentic session state
|
|
41
|
-
* @typedef {Object} AgenticSession
|
|
42
|
-
* @property {string} id - Unique session ID
|
|
43
|
-
* @property {string} task - Original task description
|
|
44
|
-
* @property {'running' | 'paused' | 'waiting_input' | 'completed' | 'stopped' | 'budget_paused'} status
|
|
45
|
-
* @property {string|null} claude_session_id - Claude Code's session ID for resume
|
|
46
|
-
* @property {number} budget_usd - Total budget allocated
|
|
47
|
-
* @property {number} cost_usd - Total cost incurred so far
|
|
48
|
-
* @property {number} started_at - Timestamp when task started
|
|
49
|
-
* @property {number} updated_at - Timestamp of last update
|
|
50
|
-
* @property {number|null} completed_at - Timestamp when task completed
|
|
51
|
-
* @property {string|null} pending_question - Question waiting for user answer
|
|
52
|
-
* @property {number} turn_count - Number of execution turns
|
|
53
|
-
* @property {Array} history - Execution history
|
|
54
|
-
*/
|
|
55
|
-
|
|
56
|
-
// In-memory session store (will be replaced with Redis in relay endpoints)
|
|
57
|
-
const agenticSessions = new Map();
|
|
58
|
-
|
|
59
|
-
// Track running processes for stop functionality
|
|
60
|
-
const runningProcesses = new Map();
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Cleanup old/stale sessions to prevent memory leaks
|
|
64
|
-
* Removes sessions that:
|
|
65
|
-
* - Are completed/stopped and older than SESSION_MAX_AGE_MS
|
|
66
|
-
* - Have no activity for SESSION_MAX_AGE_MS
|
|
67
|
-
*/
|
|
68
|
-
function cleanupStaleSessions() {
|
|
69
|
-
const now = Date.now();
|
|
70
|
-
let cleanedCount = 0;
|
|
71
|
-
|
|
72
|
-
for (const [id, session] of agenticSessions) {
|
|
73
|
-
const age = now - session.started_at;
|
|
74
|
-
const lastActivity = now - session.updated_at;
|
|
75
|
-
|
|
76
|
-
// Remove finished sessions older than max age
|
|
77
|
-
const isFinished = session.status === 'completed' || session.status === 'stopped';
|
|
78
|
-
if (isFinished && age > SESSION_MAX_AGE_MS) {
|
|
79
|
-
agenticSessions.delete(id);
|
|
80
|
-
runningProcesses.delete(id);
|
|
81
|
-
cleanedCount++;
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Remove sessions with no activity for max age
|
|
86
|
-
if (lastActivity > SESSION_MAX_AGE_MS) {
|
|
87
|
-
// Kill any running process
|
|
88
|
-
const proc = runningProcesses.get(id);
|
|
89
|
-
if (proc) {
|
|
90
|
-
proc.kill('SIGTERM');
|
|
91
|
-
runningProcesses.delete(id);
|
|
92
|
-
}
|
|
93
|
-
agenticSessions.delete(id);
|
|
94
|
-
cleanedCount++;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (cleanedCount > 0) {
|
|
99
|
-
console.log(`[agentic] Cleaned up ${cleanedCount} stale sessions`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Start periodic cleanup (only in non-test environments)
|
|
104
|
-
if (process.env.NODE_ENV !== 'test') {
|
|
105
|
-
setInterval(cleanupStaleSessions, SESSION_CLEANUP_INTERVAL_MS);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Generate a unique agentic session ID using cryptographically secure random UUID
|
|
110
|
-
* @returns {string}
|
|
111
|
-
*/
|
|
112
|
-
function generateAgenticId() {
|
|
113
|
-
return `agentic-${Date.now()}-${randomUUID()}`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Execute Claude Code in headless mode
|
|
118
|
-
*
|
|
119
|
-
* @param {Object} options
|
|
120
|
-
* @param {string} options.prompt - The prompt to send to Claude
|
|
121
|
-
* @param {string} options.cwd - Working directory
|
|
122
|
-
* @param {string|null} options.resumeSessionId - Claude session ID to resume (null for new)
|
|
123
|
-
* @param {number} options.budgetUsd - Max budget in USD
|
|
124
|
-
* @param {number} options.timeoutMs - Timeout in milliseconds
|
|
125
|
-
* @param {string} options.agenticId - Agentic session ID for tracking
|
|
126
|
-
* @returns {Promise<Object>} Execution result with session_id, output, cost, etc.
|
|
127
|
-
*/
|
|
128
|
-
async function executeClaudeHeadless(options) {
|
|
129
|
-
const {
|
|
130
|
-
prompt,
|
|
131
|
-
cwd,
|
|
132
|
-
resumeSessionId,
|
|
133
|
-
budgetUsd,
|
|
134
|
-
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
135
|
-
agenticId,
|
|
136
|
-
} = options;
|
|
137
|
-
|
|
138
|
-
return new Promise((resolve, reject) => {
|
|
139
|
-
const args = [];
|
|
140
|
-
|
|
141
|
-
// Resume existing session or start new
|
|
142
|
-
if (resumeSessionId) {
|
|
143
|
-
args.push('--resume', resumeSessionId);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Add prompt
|
|
147
|
-
args.push('-p', prompt);
|
|
148
|
-
|
|
149
|
-
// Output format for structured parsing
|
|
150
|
-
args.push('--output-format', 'json');
|
|
151
|
-
|
|
152
|
-
// Budget control (Claude Code native feature)
|
|
153
|
-
if (budgetUsd > 0) {
|
|
154
|
-
args.push('--max-budget-usd', budgetUsd.toFixed(2));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Auto-approval mode
|
|
158
|
-
args.push('--permission-mode', 'bypassPermissions');
|
|
159
|
-
|
|
160
|
-
console.log(`[agentic] Executing: ${CLAUDE_CLI} ${args.join(' ')}`);
|
|
161
|
-
|
|
162
|
-
const proc = spawn(CLAUDE_CLI, args, {
|
|
163
|
-
cwd,
|
|
164
|
-
stdio: 'pipe',
|
|
165
|
-
env: {
|
|
166
|
-
...process.env,
|
|
167
|
-
CI: 'true', // Non-interactive mode
|
|
168
|
-
TELEPORTATION_AGENTIC_MODE: 'true',
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// Track process for stop functionality
|
|
173
|
-
runningProcesses.set(agenticId, proc);
|
|
174
|
-
|
|
175
|
-
// Close stdin to prevent hanging
|
|
176
|
-
proc.stdin.end();
|
|
177
|
-
|
|
178
|
-
let stdout = '';
|
|
179
|
-
let stderr = '';
|
|
180
|
-
let timedOut = false;
|
|
181
|
-
|
|
182
|
-
// Timeout handler
|
|
183
|
-
const timeout = setTimeout(() => {
|
|
184
|
-
timedOut = true;
|
|
185
|
-
proc.kill('SIGTERM');
|
|
186
|
-
}, timeoutMs);
|
|
187
|
-
|
|
188
|
-
proc.stdout.on('data', (data) => {
|
|
189
|
-
stdout += data.toString();
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
proc.stderr.on('data', (data) => {
|
|
193
|
-
stderr += data.toString();
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
proc.on('close', (code) => {
|
|
197
|
-
clearTimeout(timeout);
|
|
198
|
-
runningProcesses.delete(agenticId);
|
|
199
|
-
|
|
200
|
-
if (timedOut) {
|
|
201
|
-
resolve({
|
|
202
|
-
success: false,
|
|
203
|
-
output: stdout,
|
|
204
|
-
error: 'Execution timed out',
|
|
205
|
-
exit_code: -1,
|
|
206
|
-
session_id: null,
|
|
207
|
-
cost_usd: 0,
|
|
208
|
-
duration_ms: timeoutMs,
|
|
209
|
-
});
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Parse JSON result to extract session_id and other metadata
|
|
214
|
-
let result = {
|
|
215
|
-
success: code === 0,
|
|
216
|
-
output: stdout,
|
|
217
|
-
error: code !== 0 ? stderr || `Exit code: ${code}` : null,
|
|
218
|
-
exit_code: code,
|
|
219
|
-
session_id: null,
|
|
220
|
-
cost_usd: 0,
|
|
221
|
-
duration_ms: 0,
|
|
222
|
-
model: null,
|
|
223
|
-
usage: null,
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
// Claude outputs JSON when using --output-format json
|
|
228
|
-
const parsed = JSON.parse(stdout);
|
|
229
|
-
|
|
230
|
-
result.output = parsed.result || parsed.response || parsed.output || stdout;
|
|
231
|
-
result.session_id = parsed.session_id || null;
|
|
232
|
-
result.cost_usd = parsed.cost || parsed.total_cost_usd || 0;
|
|
233
|
-
result.duration_ms = parsed.duration_ms || 0;
|
|
234
|
-
result.model = parsed.model || null;
|
|
235
|
-
result.usage = parsed.usage || null;
|
|
236
|
-
|
|
237
|
-
// Handle error responses
|
|
238
|
-
if (parsed.is_error) {
|
|
239
|
-
result.success = false;
|
|
240
|
-
result.error = parsed.error || 'Unknown error';
|
|
241
|
-
}
|
|
242
|
-
} catch {
|
|
243
|
-
// If JSON parsing fails, use raw stdout
|
|
244
|
-
// Try to extract session_id from text output if present
|
|
245
|
-
const sessionMatch = stdout.match(/session_id["\s:]+([a-zA-Z0-9_-]+)/i);
|
|
246
|
-
if (sessionMatch) {
|
|
247
|
-
result.session_id = sessionMatch[1];
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
resolve(result);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
proc.on('error', (err) => {
|
|
255
|
-
clearTimeout(timeout);
|
|
256
|
-
runningProcesses.delete(agenticId);
|
|
257
|
-
reject(err);
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Start a new agentic task
|
|
264
|
-
*
|
|
265
|
-
* @param {Object} options
|
|
266
|
-
* @param {string} options.task - Task description
|
|
267
|
-
* @param {string} options.session_id - Teleportation session ID
|
|
268
|
-
* @param {string} options.cwd - Working directory
|
|
269
|
-
* @param {number} [options.budget_usd] - Max budget in USD (default: 10)
|
|
270
|
-
* @param {Function} [options.onEvent] - Callback for agentic events
|
|
271
|
-
* @returns {Promise<AgenticSession>}
|
|
272
|
-
*/
|
|
273
|
-
export async function startAgenticTask(options) {
|
|
274
|
-
const {
|
|
275
|
-
task,
|
|
276
|
-
session_id,
|
|
277
|
-
cwd,
|
|
278
|
-
budget_usd = DEFAULT_BUDGET_USD,
|
|
279
|
-
onEvent,
|
|
280
|
-
} = options;
|
|
281
|
-
|
|
282
|
-
const agenticId = generateAgenticId();
|
|
283
|
-
const now = Date.now();
|
|
284
|
-
|
|
285
|
-
// Create agentic session
|
|
286
|
-
const session = {
|
|
287
|
-
id: agenticId,
|
|
288
|
-
teleportation_session_id: session_id,
|
|
289
|
-
task,
|
|
290
|
-
status: 'running',
|
|
291
|
-
claude_session_id: null,
|
|
292
|
-
budget_usd,
|
|
293
|
-
cost_usd: 0,
|
|
294
|
-
started_at: now,
|
|
295
|
-
updated_at: now,
|
|
296
|
-
completed_at: null,
|
|
297
|
-
pending_question: null,
|
|
298
|
-
turn_count: 0,
|
|
299
|
-
history: [],
|
|
300
|
-
cwd,
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
agenticSessions.set(agenticId, session);
|
|
304
|
-
|
|
305
|
-
// Emit started event
|
|
306
|
-
if (onEvent) {
|
|
307
|
-
onEvent({
|
|
308
|
-
type: 'agentic_started',
|
|
309
|
-
agentic_id: agenticId,
|
|
310
|
-
session_id,
|
|
311
|
-
task,
|
|
312
|
-
budget_usd,
|
|
313
|
-
timestamp: now,
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Start execution loop
|
|
318
|
-
executeAgenticLoop(agenticId, onEvent).catch(err => {
|
|
319
|
-
console.error(`[agentic] Loop error for ${agenticId}:`, err.message);
|
|
320
|
-
const s = agenticSessions.get(agenticId);
|
|
321
|
-
if (s) {
|
|
322
|
-
s.status = 'stopped';
|
|
323
|
-
s.updated_at = Date.now();
|
|
324
|
-
if (onEvent) {
|
|
325
|
-
onEvent({
|
|
326
|
-
type: 'agentic_stopped',
|
|
327
|
-
agentic_id: agenticId,
|
|
328
|
-
reason: `Error: ${err.message}`,
|
|
329
|
-
timestamp: Date.now(),
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
return session;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Main agentic execution loop
|
|
340
|
-
* Continues until complete, question, stopped, or budget exhausted
|
|
341
|
-
*
|
|
342
|
-
* @param {string} agenticId - Agentic session ID
|
|
343
|
-
* @param {Function} onEvent - Event callback
|
|
344
|
-
*/
|
|
345
|
-
async function executeAgenticLoop(agenticId, onEvent) {
|
|
346
|
-
const session = agenticSessions.get(agenticId);
|
|
347
|
-
if (!session) {
|
|
348
|
-
throw new Error(`Agentic session not found: ${agenticId}`);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
let continueExecution = true;
|
|
352
|
-
let consecutiveFailures = 0;
|
|
353
|
-
|
|
354
|
-
while (continueExecution) {
|
|
355
|
-
// Check for stop signal
|
|
356
|
-
if (session.status === 'stopped') {
|
|
357
|
-
console.log(`[agentic] Session ${agenticId} stopped by user`);
|
|
358
|
-
break;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Safety: Check max turns limit to prevent infinite loops
|
|
362
|
-
if (session.turn_count >= MAX_TURNS) {
|
|
363
|
-
session.status = 'stopped';
|
|
364
|
-
session.updated_at = Date.now();
|
|
365
|
-
console.log(`[agentic] Session ${agenticId} reached max turns limit (${MAX_TURNS})`);
|
|
366
|
-
if (onEvent) {
|
|
367
|
-
onEvent({
|
|
368
|
-
type: 'agentic_stopped',
|
|
369
|
-
agentic_id: agenticId,
|
|
370
|
-
reason: `Max turns limit reached (${MAX_TURNS})`,
|
|
371
|
-
cost_usd: session.cost_usd,
|
|
372
|
-
turn_count: session.turn_count,
|
|
373
|
-
timestamp: Date.now(),
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
break;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Check for pause
|
|
380
|
-
if (session.status === 'paused') {
|
|
381
|
-
console.log(`[agentic] Session ${agenticId} paused, waiting for resume`);
|
|
382
|
-
try {
|
|
383
|
-
await waitForResume(agenticId);
|
|
384
|
-
} catch (timeoutError) {
|
|
385
|
-
// Timeout occurred - session already marked as stopped in waitForResume
|
|
386
|
-
console.log(`[agentic] Session ${agenticId} pause timeout: ${timeoutError.message}`);
|
|
387
|
-
if (onEvent) {
|
|
388
|
-
onEvent({
|
|
389
|
-
type: 'agentic_stopped',
|
|
390
|
-
agentic_id: agenticId,
|
|
391
|
-
reason: 'Pause timeout - no resume received',
|
|
392
|
-
timestamp: Date.now(),
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
break;
|
|
396
|
-
}
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Check budget
|
|
401
|
-
const remainingBudget = session.budget_usd - session.cost_usd;
|
|
402
|
-
if (remainingBudget <= 0) {
|
|
403
|
-
session.status = 'budget_paused';
|
|
404
|
-
session.updated_at = Date.now();
|
|
405
|
-
console.log(`[agentic] Session ${agenticId} budget exhausted`);
|
|
406
|
-
if (onEvent) {
|
|
407
|
-
onEvent({
|
|
408
|
-
type: 'agentic_budget_hit',
|
|
409
|
-
agentic_id: agenticId,
|
|
410
|
-
cost_usd: session.cost_usd,
|
|
411
|
-
budget_usd: session.budget_usd,
|
|
412
|
-
timestamp: Date.now(),
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Build prompt for this turn
|
|
419
|
-
const prompt = session.turn_count === 0
|
|
420
|
-
? session.task
|
|
421
|
-
: 'Continue working on the task.';
|
|
422
|
-
|
|
423
|
-
// Execute Claude
|
|
424
|
-
session.turn_count++;
|
|
425
|
-
console.log(`[agentic] Turn ${session.turn_count} for ${agenticId}`);
|
|
426
|
-
|
|
427
|
-
// Re-check status immediately before expensive operation (race condition fix)
|
|
428
|
-
const currentSession = agenticSessions.get(agenticId);
|
|
429
|
-
if (!currentSession || currentSession.status === 'stopped') {
|
|
430
|
-
console.log(`[agentic] Session ${agenticId} stopped before execution`);
|
|
431
|
-
break;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
try {
|
|
435
|
-
const result = await executeClaudeHeadless({
|
|
436
|
-
prompt,
|
|
437
|
-
cwd: session.cwd,
|
|
438
|
-
resumeSessionId: session.claude_session_id,
|
|
439
|
-
budgetUsd: remainingBudget,
|
|
440
|
-
agenticId,
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
// Update session with result
|
|
444
|
-
session.claude_session_id = result.session_id || session.claude_session_id;
|
|
445
|
-
session.cost_usd += result.cost_usd || 0;
|
|
446
|
-
session.updated_at = Date.now();
|
|
447
|
-
|
|
448
|
-
// Add to history
|
|
449
|
-
session.history.push({
|
|
450
|
-
turn: session.turn_count,
|
|
451
|
-
prompt,
|
|
452
|
-
output: result.output?.slice(0, 2000), // Truncate for storage
|
|
453
|
-
cost_usd: result.cost_usd,
|
|
454
|
-
success: result.success,
|
|
455
|
-
timestamp: Date.now(),
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
// Trim history to prevent unbounded growth
|
|
459
|
-
if (session.history.length > MAX_HISTORY_SIZE) {
|
|
460
|
-
session.history = session.history.slice(-MAX_HISTORY_SIZE);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Track consecutive failures for error recovery
|
|
464
|
-
if (!result.success) {
|
|
465
|
-
consecutiveFailures++;
|
|
466
|
-
console.error(`[agentic] Turn ${session.turn_count} failed (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}):`, result.error);
|
|
467
|
-
|
|
468
|
-
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
469
|
-
session.status = 'stopped';
|
|
470
|
-
session.updated_at = Date.now();
|
|
471
|
-
console.log(`[agentic] Session ${agenticId} stopped due to ${MAX_CONSECUTIVE_FAILURES} consecutive failures`);
|
|
472
|
-
if (onEvent) {
|
|
473
|
-
onEvent({
|
|
474
|
-
type: 'agentic_stopped',
|
|
475
|
-
agentic_id: agenticId,
|
|
476
|
-
reason: `${MAX_CONSECUTIVE_FAILURES} consecutive failures`,
|
|
477
|
-
cost_usd: session.cost_usd,
|
|
478
|
-
turn_count: session.turn_count,
|
|
479
|
-
timestamp: Date.now(),
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
break;
|
|
483
|
-
}
|
|
484
|
-
} else {
|
|
485
|
-
// Reset failure counter on success
|
|
486
|
-
consecutiveFailures = 0;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Classify the response
|
|
490
|
-
const classification = classifyResponse(result);
|
|
491
|
-
console.log(`[agentic] Classification: ${classification.action} (confidence: ${classification.confidence})`);
|
|
492
|
-
|
|
493
|
-
// Determine next action based on classification
|
|
494
|
-
switch (classification.action) {
|
|
495
|
-
case 'complete':
|
|
496
|
-
session.status = 'completed';
|
|
497
|
-
session.completed_at = Date.now();
|
|
498
|
-
session.updated_at = Date.now();
|
|
499
|
-
continueExecution = false;
|
|
500
|
-
console.log(`[agentic] Task completed for ${agenticId}`);
|
|
501
|
-
if (onEvent) {
|
|
502
|
-
onEvent({
|
|
503
|
-
type: 'agentic_completed',
|
|
504
|
-
agentic_id: agenticId,
|
|
505
|
-
output: result.output,
|
|
506
|
-
cost_usd: session.cost_usd,
|
|
507
|
-
turn_count: session.turn_count,
|
|
508
|
-
timestamp: Date.now(),
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
break;
|
|
512
|
-
|
|
513
|
-
case 'question':
|
|
514
|
-
session.status = 'waiting_input';
|
|
515
|
-
session.pending_question = classification.questionText;
|
|
516
|
-
session.updated_at = Date.now();
|
|
517
|
-
continueExecution = false;
|
|
518
|
-
console.log(`[agentic] Question detected for ${agenticId}: ${classification.questionText?.slice(0, 100)}`);
|
|
519
|
-
if (onEvent) {
|
|
520
|
-
onEvent({
|
|
521
|
-
type: 'agentic_question',
|
|
522
|
-
agentic_id: agenticId,
|
|
523
|
-
question: classification.questionText,
|
|
524
|
-
confidence: classification.confidence,
|
|
525
|
-
timestamp: Date.now(),
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
break;
|
|
529
|
-
|
|
530
|
-
case 'continue':
|
|
531
|
-
default:
|
|
532
|
-
// Continue working - loop will iterate
|
|
533
|
-
console.log(`[agentic] Continuing execution for ${agenticId}`);
|
|
534
|
-
break;
|
|
535
|
-
}
|
|
536
|
-
} catch (error) {
|
|
537
|
-
console.error(`[agentic] Execution error for ${agenticId}:`, error.message);
|
|
538
|
-
session.status = 'stopped';
|
|
539
|
-
session.updated_at = Date.now();
|
|
540
|
-
continueExecution = false;
|
|
541
|
-
if (onEvent) {
|
|
542
|
-
onEvent({
|
|
543
|
-
type: 'agentic_stopped',
|
|
544
|
-
agentic_id: agenticId,
|
|
545
|
-
reason: `Error: ${error.message}`,
|
|
546
|
-
timestamp: Date.now(),
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* Wait for an agentic session to be resumed
|
|
555
|
-
* @param {string} agenticId
|
|
556
|
-
* @returns {Promise<void>}
|
|
557
|
-
*/
|
|
558
|
-
/**
|
|
559
|
-
* Wait for an agentic session to be resumed with timeout protection
|
|
560
|
-
* @param {string} agenticId
|
|
561
|
-
* @param {number} timeoutMs - Maximum wait time (default: 30 minutes)
|
|
562
|
-
* @returns {Promise<void>}
|
|
563
|
-
* @throws {Error} If timeout is reached
|
|
564
|
-
*/
|
|
565
|
-
async function waitForResume(agenticId, timeoutMs = WAIT_FOR_RESUME_TIMEOUT_MS) {
|
|
566
|
-
return new Promise((resolve, reject) => {
|
|
567
|
-
const startTime = Date.now();
|
|
568
|
-
|
|
569
|
-
const checkInterval = setInterval(() => {
|
|
570
|
-
const session = agenticSessions.get(agenticId);
|
|
571
|
-
|
|
572
|
-
// Check for timeout
|
|
573
|
-
if (Date.now() - startTime > timeoutMs) {
|
|
574
|
-
clearInterval(checkInterval);
|
|
575
|
-
console.log(`[agentic] Session ${agenticId} resume timeout after ${timeoutMs}ms`);
|
|
576
|
-
|
|
577
|
-
// Mark session as stopped due to timeout
|
|
578
|
-
if (session) {
|
|
579
|
-
session.status = 'stopped';
|
|
580
|
-
session.updated_at = Date.now();
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
reject(new Error(`Resume timeout after ${timeoutMs}ms`));
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// Check if session no longer paused
|
|
588
|
-
if (!session || session.status !== 'paused') {
|
|
589
|
-
clearInterval(checkInterval);
|
|
590
|
-
resolve();
|
|
591
|
-
}
|
|
592
|
-
}, POLLING_INTERVAL_MS);
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
/**
|
|
597
|
-
* Stop an agentic task
|
|
598
|
-
*
|
|
599
|
-
* @param {string} agenticId - Agentic session ID
|
|
600
|
-
* @returns {{ success: boolean, reason?: string }}
|
|
601
|
-
*/
|
|
602
|
-
export function stopAgenticTask(agenticId) {
|
|
603
|
-
const session = agenticSessions.get(agenticId);
|
|
604
|
-
if (!session) {
|
|
605
|
-
return { success: false, reason: 'Session not found' };
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// Kill running process if any
|
|
609
|
-
const proc = runningProcesses.get(agenticId);
|
|
610
|
-
if (proc) {
|
|
611
|
-
proc.kill('SIGTERM');
|
|
612
|
-
runningProcesses.delete(agenticId);
|
|
613
|
-
console.log(`[agentic] Killed process for ${agenticId}`);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
session.status = 'stopped';
|
|
617
|
-
session.updated_at = Date.now();
|
|
618
|
-
|
|
619
|
-
return { success: true };
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
/**
|
|
623
|
-
* Pause an agentic task
|
|
624
|
-
*
|
|
625
|
-
* @param {string} agenticId - Agentic session ID
|
|
626
|
-
* @returns {{ success: boolean, reason?: string }}
|
|
627
|
-
*/
|
|
628
|
-
export function pauseAgenticTask(agenticId) {
|
|
629
|
-
const session = agenticSessions.get(agenticId);
|
|
630
|
-
if (!session) {
|
|
631
|
-
return { success: false, reason: 'Session not found' };
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
if (session.status !== 'running') {
|
|
635
|
-
return { success: false, reason: `Cannot pause session in ${session.status} status` };
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
session.status = 'paused';
|
|
639
|
-
session.updated_at = Date.now();
|
|
640
|
-
|
|
641
|
-
return { success: true };
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
/**
|
|
645
|
-
* Resume a paused agentic task
|
|
646
|
-
*
|
|
647
|
-
* @param {string} agenticId - Agentic session ID
|
|
648
|
-
* @param {Function} [onEvent] - Event callback
|
|
649
|
-
* @returns {{ success: boolean, reason?: string }}
|
|
650
|
-
*/
|
|
651
|
-
export function resumeAgenticTask(agenticId, onEvent) {
|
|
652
|
-
const session = agenticSessions.get(agenticId);
|
|
653
|
-
if (!session) {
|
|
654
|
-
return { success: false, reason: 'Session not found' };
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
if (session.status !== 'paused' && session.status !== 'budget_paused') {
|
|
658
|
-
return { success: false, reason: `Cannot resume session in ${session.status} status` };
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
session.status = 'running';
|
|
662
|
-
session.updated_at = Date.now();
|
|
663
|
-
|
|
664
|
-
// Restart execution loop
|
|
665
|
-
executeAgenticLoop(agenticId, onEvent).catch(err => {
|
|
666
|
-
console.error(`[agentic] Resume loop error for ${agenticId}:`, err.message);
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
return { success: true };
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
/**
|
|
673
|
-
* Answer a question for an agentic task
|
|
674
|
-
*
|
|
675
|
-
* @param {string} agenticId - Agentic session ID
|
|
676
|
-
* @param {string} answer - User's answer to the question
|
|
677
|
-
* @param {Function} [onEvent] - Event callback
|
|
678
|
-
* @returns {{ success: boolean, reason?: string }}
|
|
679
|
-
*/
|
|
680
|
-
export async function answerAgenticQuestion(agenticId, answer, onEvent) {
|
|
681
|
-
const session = agenticSessions.get(agenticId);
|
|
682
|
-
if (!session) {
|
|
683
|
-
return { success: false, reason: 'Session not found' };
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
if (session.status !== 'waiting_input') {
|
|
687
|
-
return { success: false, reason: `Cannot answer question in ${session.status} status` };
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// Emit user input event
|
|
691
|
-
if (onEvent) {
|
|
692
|
-
onEvent({
|
|
693
|
-
type: 'agentic_user_input',
|
|
694
|
-
agentic_id: agenticId,
|
|
695
|
-
question: session.pending_question,
|
|
696
|
-
answer,
|
|
697
|
-
timestamp: Date.now(),
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// Clear pending question and set status to running
|
|
702
|
-
session.pending_question = null;
|
|
703
|
-
session.status = 'running';
|
|
704
|
-
session.updated_at = Date.now();
|
|
705
|
-
|
|
706
|
-
// Execute the next turn with the user's answer
|
|
707
|
-
const remainingBudget = session.budget_usd - session.cost_usd;
|
|
708
|
-
|
|
709
|
-
try {
|
|
710
|
-
session.turn_count++;
|
|
711
|
-
const result = await executeClaudeHeadless({
|
|
712
|
-
prompt: answer,
|
|
713
|
-
cwd: session.cwd,
|
|
714
|
-
resumeSessionId: session.claude_session_id,
|
|
715
|
-
budgetUsd: remainingBudget,
|
|
716
|
-
agenticId,
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
// Update session
|
|
720
|
-
session.claude_session_id = result.session_id || session.claude_session_id;
|
|
721
|
-
session.cost_usd += result.cost_usd || 0;
|
|
722
|
-
session.updated_at = Date.now();
|
|
723
|
-
|
|
724
|
-
// Add to history
|
|
725
|
-
session.history.push({
|
|
726
|
-
turn: session.turn_count,
|
|
727
|
-
prompt: `[User Answer] ${answer}`,
|
|
728
|
-
output: result.output?.slice(0, 2000),
|
|
729
|
-
cost_usd: result.cost_usd,
|
|
730
|
-
success: result.success,
|
|
731
|
-
timestamp: Date.now(),
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
// Resume the execution loop
|
|
735
|
-
executeAgenticLoop(agenticId, onEvent).catch(err => {
|
|
736
|
-
console.error(`[agentic] Answer loop error for ${agenticId}:`, err.message);
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
return { success: true };
|
|
740
|
-
} catch (error) {
|
|
741
|
-
session.status = 'stopped';
|
|
742
|
-
session.updated_at = Date.now();
|
|
743
|
-
return { success: false, reason: error.message };
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
/**
|
|
748
|
-
* Get agentic session status
|
|
749
|
-
*
|
|
750
|
-
* @param {string} agenticId - Agentic session ID
|
|
751
|
-
* @returns {AgenticSession|null}
|
|
752
|
-
*/
|
|
753
|
-
export function getAgenticSession(agenticId) {
|
|
754
|
-
return agenticSessions.get(agenticId) || null;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
/**
|
|
758
|
-
* List all agentic sessions for a teleportation session
|
|
759
|
-
*
|
|
760
|
-
* @param {string} session_id - Teleportation session ID
|
|
761
|
-
* @returns {AgenticSession[]}
|
|
762
|
-
*/
|
|
763
|
-
export function listAgenticSessions(session_id) {
|
|
764
|
-
const sessions = [];
|
|
765
|
-
for (const [, session] of agenticSessions) {
|
|
766
|
-
if (session.teleportation_session_id === session_id) {
|
|
767
|
-
sessions.push(session);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
return sessions;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
/**
|
|
774
|
-
* Increase budget for an agentic task
|
|
775
|
-
*
|
|
776
|
-
* @param {string} agenticId - Agentic session ID
|
|
777
|
-
* @param {number} additionalBudget - Additional budget in USD
|
|
778
|
-
* @returns {{ success: boolean, new_budget?: number, reason?: string }}
|
|
779
|
-
*/
|
|
780
|
-
export function increaseBudget(agenticId, additionalBudget) {
|
|
781
|
-
const session = agenticSessions.get(agenticId);
|
|
782
|
-
if (!session) {
|
|
783
|
-
return { success: false, reason: 'Session not found' };
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
if (additionalBudget <= 0) {
|
|
787
|
-
return { success: false, reason: 'Additional budget must be positive' };
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
session.budget_usd += additionalBudget;
|
|
791
|
-
session.updated_at = Date.now();
|
|
792
|
-
|
|
793
|
-
return { success: true, new_budget: session.budget_usd };
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
// Export for testing
|
|
797
|
-
export const __test = {
|
|
798
|
-
agenticSessions,
|
|
799
|
-
runningProcesses,
|
|
800
|
-
executeClaudeHeadless,
|
|
801
|
-
executeAgenticLoop,
|
|
802
|
-
generateAgenticId,
|
|
803
|
-
};
|