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.
Files changed (48) hide show
  1. package/.claude/hooks/config-loader.mjs +88 -34
  2. package/.claude/hooks/permission_request.mjs +392 -82
  3. package/.claude/hooks/post_tool_use.mjs +90 -0
  4. package/.claude/hooks/pre_tool_use.mjs +247 -305
  5. package/.claude/hooks/session-register.mjs +94 -105
  6. package/.claude/hooks/session_end.mjs +41 -42
  7. package/.claude/hooks/session_start.mjs +45 -60
  8. package/.claude/hooks/stop.mjs +752 -99
  9. package/.claude/hooks/user_prompt_submit.mjs +26 -3
  10. package/README.md +7 -0
  11. package/lib/auth/api-key.js +12 -0
  12. package/lib/auth/token-refresh.js +286 -0
  13. package/lib/cli/daemon-commands.js +1 -1
  14. package/lib/cli/teleport-commands.js +469 -0
  15. package/lib/daemon/daemon-v2.js +104 -0
  16. package/lib/daemon/lifecycle.js +56 -171
  17. package/lib/daemon/response-classifier.js +15 -1
  18. package/lib/daemon/services/index.js +3 -0
  19. package/lib/daemon/services/polling-service.js +173 -0
  20. package/lib/daemon/services/queue-service.js +318 -0
  21. package/lib/daemon/services/session-service.js +115 -0
  22. package/lib/daemon/state.js +35 -0
  23. package/lib/daemon/task-executor-v2.js +413 -0
  24. package/lib/daemon/task-executor.js +1235 -0
  25. package/lib/daemon/teleportation-daemon.js +770 -25
  26. package/lib/daemon/timeline-analyzer.js +215 -0
  27. package/lib/daemon/transcript-ingestion.js +696 -0
  28. package/lib/daemon/utils.js +91 -0
  29. package/lib/install/installer.js +184 -20
  30. package/lib/install/uhr-installer.js +136 -0
  31. package/lib/remote/providers/base-provider.js +46 -0
  32. package/lib/remote/providers/daytona-provider.js +58 -0
  33. package/lib/remote/providers/provider-factory.js +90 -19
  34. package/lib/remote/providers/sprites-provider.js +711 -0
  35. package/lib/teleport/exporters/claude-exporter.js +302 -0
  36. package/lib/teleport/exporters/gemini-exporter.js +307 -0
  37. package/lib/teleport/exporters/index.js +93 -0
  38. package/lib/teleport/exporters/interface.js +153 -0
  39. package/lib/teleport/fork-tracker.js +415 -0
  40. package/lib/teleport/git-committer.js +337 -0
  41. package/lib/teleport/index.js +48 -0
  42. package/lib/teleport/manager.js +620 -0
  43. package/lib/teleport/session-capture.js +282 -0
  44. package/package.json +11 -5
  45. package/teleportation-cli.cjs +632 -451
  46. package/.claude/hooks/heartbeat.mjs +0 -396
  47. package/lib/daemon/agentic-executor.js +0 -803
  48. 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
- };