teleportation-cli 1.1.5 → 1.2.1

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 (42) hide show
  1. package/.claude/hooks/permission_request.mjs +326 -59
  2. package/.claude/hooks/post_tool_use.mjs +90 -0
  3. package/.claude/hooks/pre_tool_use.mjs +212 -293
  4. package/.claude/hooks/session-register.mjs +89 -104
  5. package/.claude/hooks/session_end.mjs +41 -42
  6. package/.claude/hooks/session_start.mjs +45 -60
  7. package/.claude/hooks/stop.mjs +752 -99
  8. package/.claude/hooks/user_prompt_submit.mjs +26 -3
  9. package/lib/cli/daemon-commands.js +1 -1
  10. package/lib/cli/teleport-commands.js +469 -0
  11. package/lib/daemon/daemon-v2.js +104 -0
  12. package/lib/daemon/lifecycle.js +56 -171
  13. package/lib/daemon/services/index.js +3 -0
  14. package/lib/daemon/services/polling-service.js +173 -0
  15. package/lib/daemon/services/queue-service.js +318 -0
  16. package/lib/daemon/services/session-service.js +115 -0
  17. package/lib/daemon/state.js +35 -0
  18. package/lib/daemon/task-executor-v2.js +413 -0
  19. package/lib/daemon/task-executor.js +270 -96
  20. package/lib/daemon/teleportation-daemon.js +709 -126
  21. package/lib/daemon/timeline-analyzer.js +215 -0
  22. package/lib/daemon/transcript-ingestion.js +696 -0
  23. package/lib/daemon/utils.js +91 -0
  24. package/lib/install/installer.js +184 -20
  25. package/lib/install/uhr-installer.js +136 -0
  26. package/lib/remote/providers/base-provider.js +46 -0
  27. package/lib/remote/providers/daytona-provider.js +58 -0
  28. package/lib/remote/providers/provider-factory.js +90 -19
  29. package/lib/remote/providers/sprites-provider.js +711 -0
  30. package/lib/teleport/exporters/claude-exporter.js +302 -0
  31. package/lib/teleport/exporters/gemini-exporter.js +307 -0
  32. package/lib/teleport/exporters/index.js +93 -0
  33. package/lib/teleport/exporters/interface.js +153 -0
  34. package/lib/teleport/fork-tracker.js +415 -0
  35. package/lib/teleport/git-committer.js +337 -0
  36. package/lib/teleport/index.js +48 -0
  37. package/lib/teleport/manager.js +620 -0
  38. package/lib/teleport/session-capture.js +282 -0
  39. package/package.json +6 -2
  40. package/teleportation-cli.cjs +488 -453
  41. package/.claude/hooks/heartbeat.mjs +0 -396
  42. package/lib/daemon/pid-manager.js +0 -183
@@ -13,7 +13,7 @@
13
13
  * - Captures `session_id` from JSON result for conversation continuity
14
14
  * - Uses `--resume <session-id>` for subsequent turns
15
15
  * - Budget control via `--max-budget-usd`
16
- * - Auto-approval via `--permission-mode bypassPermissions`
16
+ * - Hooks fire normally (approvals go to mobile app)
17
17
  *
18
18
  * @module lib/daemon/task-executor
19
19
  */
@@ -74,6 +74,7 @@ export function removeTaskLock(taskId) {
74
74
  * @property {string} task - Original task description
75
75
  * @property {'running' | 'paused' | 'waiting_input' | 'completed' | 'stopped' | 'budget_paused'} status
76
76
  * @property {string|null} claude_session_id - Claude Code's session ID for resume
77
+ * @property {boolean} auto_continue - Whether to automatically continue after each turn
77
78
  * @property {number} budget_usd - Total budget allocated
78
79
  * @property {number} cost_usd - Total cost incurred so far
79
80
  * @property {number} started_at - Timestamp when task started
@@ -226,6 +227,7 @@ function generateTaskId() {
226
227
  * @param {number} options.budgetUsd - Max budget in USD
227
228
  * @param {number} options.timeoutMs - Timeout in milliseconds
228
229
  * @param {string} options.taskId - Task session ID for tracking
230
+ * @param {Function} [options.onUpdate] - Optional callback for streaming updates
229
231
  * @returns {Promise<Object>} Execution result with session_id, output, cost, etc.
230
232
  */
231
233
  async function executeClaudeHeadless(options) {
@@ -236,6 +238,8 @@ async function executeClaudeHeadless(options) {
236
238
  budgetUsd,
237
239
  timeoutMs = DEFAULT_TIMEOUT_MS,
238
240
  taskId,
241
+ parentSessionId,
242
+ onUpdate,
239
243
  } = options;
240
244
 
241
245
  return new Promise((resolve, reject) => {
@@ -249,16 +253,18 @@ async function executeClaudeHeadless(options) {
249
253
  // Add prompt
250
254
  args.push('-p', prompt);
251
255
 
252
- // Output format for structured parsing
253
- args.push('--output-format', 'json');
256
+ // Output format for structured parsing - using stream-json for real-time metadata
257
+ args.push('--output-format', 'stream-json');
258
+ args.push('--verbose');
254
259
 
255
260
  // Budget control (Claude Code native feature)
256
261
  if (budgetUsd > 0) {
257
262
  args.push('--max-budget-usd', budgetUsd.toFixed(2));
258
263
  }
259
264
 
260
- // Auto-approval mode
261
- args.push('--permission-mode', 'bypassPermissions');
265
+ // Hooks enabled: approvals route to mobile app
266
+ // Session must be in "away" mode for hooks to wait for remote approval
267
+ // (Daemon sets away mode before calling startTask)
262
268
 
263
269
  console.log(`[task] Executing: ${CLAUDE_CLI} ${args.join(' ')}`);
264
270
 
@@ -269,6 +275,8 @@ async function executeClaudeHeadless(options) {
269
275
  ...process.env,
270
276
  CI: 'true', // Non-interactive mode
271
277
  TELEPORTATION_TASK_MODE: 'true',
278
+ // Pass parent session ID so hooks can log approvals to correct timeline
279
+ ...(parentSessionId && { TELEPORTATION_PARENT_SESSION_ID: parentSessionId }),
272
280
  },
273
281
  });
274
282
 
@@ -281,6 +289,27 @@ async function executeClaudeHeadless(options) {
281
289
  let stdout = '';
282
290
  let stderr = '';
283
291
  let timedOut = false;
292
+ let buffer = '';
293
+
294
+ // Final result structure
295
+ let result = {
296
+ success: false,
297
+ output: '',
298
+ error: null,
299
+ exit_code: 0,
300
+ session_id: null,
301
+ cost_usd: 0,
302
+ duration_ms: 0,
303
+ model: null,
304
+ usage: {
305
+ tools_used: 0,
306
+ input_tokens: 0,
307
+ output_tokens: 0,
308
+ cache_creation_input_tokens: 0,
309
+ cache_read_input_tokens: 0
310
+ },
311
+ tool_calls: [],
312
+ };
284
313
 
285
314
  // Timeout handler
286
315
  const timeout = setTimeout(() => {
@@ -289,7 +318,56 @@ async function executeClaudeHeadless(options) {
289
318
  }, timeoutMs);
290
319
 
291
320
  proc.stdout.on('data', (data) => {
292
- stdout += data.toString();
321
+ const chunk = data.toString();
322
+ stdout += chunk;
323
+ buffer += chunk;
324
+
325
+ // Process stream-json lines
326
+ const lines = buffer.split('\n');
327
+ buffer = lines.pop(); // Keep last incomplete line
328
+
329
+ for (const line of lines) {
330
+ if (!line.trim()) continue;
331
+ try {
332
+ // Debug: console.debug(`[stream] Line: ${line.slice(0, 100)}`);
333
+ const msg = JSON.parse(line);
334
+
335
+ // Accumulate metadata from stream
336
+ if (msg.type === 'system' && msg.subtype === 'init' && msg.session_id) {
337
+ result.session_id = msg.session_id;
338
+ }
339
+
340
+ if (msg.type === 'assistant' && msg.message?.content) {
341
+ const content = msg.message.content;
342
+ for (const block of content) {
343
+ if (block.type === 'text') {
344
+ result.output += block.text;
345
+ if (onUpdate) onUpdate({ type: 'text', text: block.text });
346
+ } else if (block.type === 'tool_use') {
347
+ result.usage.tools_used++;
348
+ result.tool_calls.push(block);
349
+ if (onUpdate) onUpdate({ type: 'tool_use', tool: block.name });
350
+ }
351
+ }
352
+ }
353
+
354
+ if (msg.type === 'usage' || (msg.type === 'assistant' && msg.message?.usage)) {
355
+ const usage = msg.usage || msg.message.usage;
356
+ result.usage.input_tokens += usage.input_tokens || 0;
357
+ result.usage.output_tokens += usage.output_tokens || 0;
358
+ result.usage.cache_creation_input_tokens += usage.cache_creation_input_tokens || 0;
359
+ result.usage.cache_read_input_tokens += usage.cache_read_input_tokens || 0;
360
+ }
361
+
362
+ if (msg.type === 'result') {
363
+ result.success = !msg.is_error;
364
+ if (msg.is_error) result.error = msg.error;
365
+ if (msg.total_cost_usd) result.cost_usd = msg.total_cost_usd;
366
+ }
367
+ } catch (e) {
368
+ // Non-JSON line, ignore or log
369
+ }
370
+ }
293
371
  });
294
372
 
295
373
  proc.stderr.on('data', (data) => {
@@ -300,54 +378,30 @@ async function executeClaudeHeadless(options) {
300
378
  clearTimeout(timeout);
301
379
  runningProcesses.delete(taskId);
302
380
 
381
+ result.exit_code = code;
382
+ result.duration_ms = Date.now() - (result.start_time || Date.now());
383
+
303
384
  if (timedOut) {
304
- resolve({
305
- success: false,
306
- output: stdout,
307
- error: 'Execution timed out',
308
- exit_code: -1,
309
- session_id: null,
310
- cost_usd: 0,
311
- duration_ms: timeoutMs,
312
- });
313
- return;
385
+ result.success = false;
386
+ result.error = 'Execution timed out';
387
+ result.exit_code = -1;
388
+ } else if (code !== 0 && !result.error) {
389
+ result.success = false;
390
+ result.error = stderr || `Exit code: ${code}`;
391
+ } else if (code === 0) {
392
+ result.success = true;
314
393
  }
315
394
 
316
- // Parse JSON result to extract session_id and other metadata
317
- let result = {
318
- success: code === 0,
319
- output: stdout,
320
- error: code !== 0 ? stderr || `Exit code: ${code}` : null,
321
- exit_code: code,
322
- session_id: null,
323
- cost_usd: 0,
324
- duration_ms: 0,
325
- model: null,
326
- usage: null,
327
- };
328
-
329
- try {
330
- // Claude outputs JSON when using --output-format json
331
- const parsed = JSON.parse(stdout);
332
-
333
- result.output = parsed.result || parsed.response || parsed.output || stdout;
334
- result.session_id = parsed.session_id || null;
335
- result.cost_usd = parsed.cost || parsed.total_cost_usd || 0;
336
- result.duration_ms = parsed.duration_ms || 0;
337
- result.model = parsed.model || null;
338
- result.usage = parsed.usage || null;
339
-
340
- // Handle error responses
341
- if (parsed.is_error) {
342
- result.success = false;
343
- result.error = parsed.error || 'Unknown error';
344
- }
345
- } catch {
346
- // If JSON parsing fails, use raw stdout
347
- // Try to extract session_id from text output if present
348
- const sessionMatch = stdout.match(/session_id["\s:]+([a-zA-Z0-9_-]+)/i);
349
- if (sessionMatch) {
350
- result.session_id = sessionMatch[1];
395
+ // If output is still empty but we have stdout that wasn't stream-json,
396
+ // fall back to raw stdout
397
+ if (!result.output && stdout) {
398
+ try {
399
+ const parsed = JSON.parse(stdout);
400
+ result.output = parsed.result || parsed.response || parsed.output || stdout;
401
+ result.session_id = result.session_id || parsed.session_id;
402
+ result.cost_usd = result.cost_usd || parsed.cost || 0;
403
+ } catch {
404
+ result.output = stdout;
351
405
  }
352
406
  }
353
407
 
@@ -370,6 +424,7 @@ async function executeClaudeHeadless(options) {
370
424
  * @param {string} options.session_id - Teleportation session ID
371
425
  * @param {string} options.cwd - Working directory
372
426
  * @param {number} [options.budget_usd] - Max budget in USD (default: 10)
427
+ * @param {boolean} [options.auto_continue=false] - Auto-continue after each turn (default: false). When disabled, task pauses after each turn and waits for user message.
373
428
  * @param {Function} [options.onEvent] - Callback for task events
374
429
  * @returns {Promise<TaskSession>}
375
430
  */
@@ -379,9 +434,14 @@ export async function startTask(options) {
379
434
  session_id,
380
435
  cwd,
381
436
  budget_usd = DEFAULT_BUDGET_USD,
437
+ parent_claude_session_id = null,
438
+ auto_continue: rawAutoContinue = false,
382
439
  onEvent,
383
440
  } = options;
384
441
 
442
+ // Validate auto_continue parameter - ensure it's a boolean
443
+ const auto_continue = Boolean(rawAutoContinue ?? false);
444
+
385
445
  const taskId = options.task_id || generateTaskId();
386
446
  const now = Date.now();
387
447
  const lockPath = path.join(TASKS_LOCK_DIR, `${taskId}.pid`);
@@ -456,7 +516,9 @@ export async function startTask(options) {
456
516
  teleportation_session_id: session_id,
457
517
  task,
458
518
  status: 'running',
459
- claude_session_id: null,
519
+ claude_session_id: null, // Will be set to child session on first execution
520
+ parent_claude_session_id, // Parent session to resume for context
521
+ auto_continue, // Whether to automatically continue or wait for user messages
460
522
  budget_usd,
461
523
  cost_usd: 0,
462
524
  started_at: now,
@@ -474,6 +536,7 @@ export async function startTask(options) {
474
536
  if (onEvent) {
475
537
  onEvent({
476
538
  type: 'task_started',
539
+ source: 'autonomous_task',
477
540
  task_id: taskId,
478
541
  session_id,
479
542
  task,
@@ -501,6 +564,7 @@ export async function startTask(options) {
501
564
  if (onEvent) {
502
565
  onEvent({
503
566
  type: 'task_stopped',
567
+ source: 'autonomous_task',
504
568
  task_id: taskId,
505
569
  reason: `Error: ${err.message}`,
506
570
  timestamp: Date.now(),
@@ -543,6 +607,7 @@ async function executeTaskLoop(taskId, onEvent) {
543
607
  if (onEvent) {
544
608
  onEvent({
545
609
  type: 'task_stopped',
610
+ source: 'autonomous_task',
546
611
  task_id: taskId,
547
612
  reason: `Max turns limit reached (${MAX_TURNS})`,
548
613
  cost_usd: session.cost_usd,
@@ -564,6 +629,7 @@ async function executeTaskLoop(taskId, onEvent) {
564
629
  if (onEvent) {
565
630
  onEvent({
566
631
  type: 'task_stopped',
632
+ source: 'autonomous_task',
567
633
  task_id: taskId,
568
634
  reason: 'Pause timeout - no resume received',
569
635
  timestamp: Date.now(),
@@ -583,6 +649,7 @@ async function executeTaskLoop(taskId, onEvent) {
583
649
  if (onEvent) {
584
650
  onEvent({
585
651
  type: 'task_budget_hit',
652
+ source: 'autonomous_task',
586
653
  task_id: taskId,
587
654
  cost_usd: session.cost_usd,
588
655
  budget_usd: session.budget_usd,
@@ -593,9 +660,55 @@ async function executeTaskLoop(taskId, onEvent) {
593
660
  }
594
661
 
595
662
  // Build prompt for this turn
596
- const prompt = session.turn_count === 0
597
- ? session.task
598
- : 'Continue working on the task.';
663
+ let prompt;
664
+ if (session.turn_count === 0) {
665
+ // First turn: use the original task description
666
+ prompt = session.task;
667
+ } else if (session.auto_continue) {
668
+ // Auto-continuation enabled: use generic continuation
669
+ prompt = 'Continue working on the task.';
670
+ } else {
671
+ // Auto-continuation disabled: pause and wait for user message
672
+ session.status = 'paused';
673
+ session.updated_at = Date.now();
674
+ console.log(`[task] Turn ${session.turn_count + 1}: Auto-continue disabled, pausing for user input`);
675
+ if (onEvent) {
676
+ onEvent({
677
+ type: 'task_paused',
678
+ source: 'autonomous_task',
679
+ task_id: taskId,
680
+ reason: 'Waiting for user message (auto_continue disabled)',
681
+ turn_count: session.turn_count,
682
+ timestamp: Date.now(),
683
+ });
684
+ }
685
+ // Wait for resume with user message
686
+ try {
687
+ await waitForResume(taskId);
688
+ // Atomically get and clear pending_question to prevent race conditions
689
+ const userMessage = atomicGetAndClearMessage(taskId);
690
+ if (!userMessage) {
691
+ console.log(`[task] Resumed without user message, stopping task ${taskId}`);
692
+ session.status = 'stopped';
693
+ session.updated_at = Date.now();
694
+ break;
695
+ }
696
+ // Use the user's message as the prompt
697
+ prompt = userMessage;
698
+ } catch (timeoutError) {
699
+ console.log(`[task] Session ${taskId} pause timeout: ${timeoutError.message}`);
700
+ if (onEvent) {
701
+ onEvent({
702
+ type: 'task_stopped',
703
+ source: 'autonomous_task',
704
+ task_id: taskId,
705
+ reason: 'Pause timeout - no user message received',
706
+ timestamp: Date.now(),
707
+ });
708
+ }
709
+ break;
710
+ }
711
+ }
599
712
 
600
713
  // Execute Claude
601
714
  session.turn_count++;
@@ -609,12 +722,37 @@ async function executeTaskLoop(taskId, onEvent) {
609
722
  }
610
723
 
611
724
  try {
725
+ // Determine which session to resume:
726
+ // - Turn 1: Resume parent session for context (if available)
727
+ // - Turn 2+: Resume child session from previous turn
728
+ const resumeSessionId = session.claude_session_id
729
+ ? session.claude_session_id // Use child session from previous turns
730
+ : session.parent_claude_session_id; // Use parent session on first turn
731
+
732
+ if (resumeSessionId && session.turn_count === 1) {
733
+ console.log(`[task] Turn 1: Resuming parent session ${resumeSessionId} for context`);
734
+ } else if (resumeSessionId) {
735
+ console.log(`[task] Turn ${session.turn_count}: Resuming child session ${resumeSessionId}`);
736
+ }
737
+
612
738
  const result = await executeClaudeHeadless({
613
739
  prompt,
614
740
  cwd: session.cwd,
615
- resumeSessionId: session.claude_session_id,
741
+ resumeSessionId,
616
742
  budgetUsd: remainingBudget,
617
743
  taskId,
744
+ parentSessionId: session.teleportation_session_id,
745
+ onUpdate: (update) => {
746
+ if (onEvent) {
747
+ onEvent({
748
+ type: 'task_progress',
749
+ source: 'autonomous_task',
750
+ task_id: taskId,
751
+ update,
752
+ timestamp: Date.now(),
753
+ });
754
+ }
755
+ }
618
756
  });
619
757
 
620
758
  // Update session with result
@@ -629,6 +767,7 @@ async function executeTaskLoop(taskId, onEvent) {
629
767
  output: result.output?.slice(0, 2000), // Truncate for storage
630
768
  cost_usd: result.cost_usd,
631
769
  success: result.success,
770
+ usage: result.usage,
632
771
  timestamp: Date.now(),
633
772
  });
634
773
 
@@ -649,6 +788,7 @@ async function executeTaskLoop(taskId, onEvent) {
649
788
  if (onEvent) {
650
789
  onEvent({
651
790
  type: 'task_stopped',
791
+ source: 'autonomous_task',
652
792
  task_id: taskId,
653
793
  reason: `${MAX_CONSECUTIVE_FAILURES} consecutive failures`,
654
794
  cost_usd: session.cost_usd,
@@ -698,6 +838,7 @@ async function executeTaskLoop(taskId, onEvent) {
698
838
  if (onEvent) {
699
839
  onEvent({
700
840
  type: 'task_completed',
841
+ source: 'autonomous_task',
701
842
  task_id: taskId,
702
843
  output: result.output,
703
844
  cost_usd: session.cost_usd,
@@ -716,6 +857,7 @@ async function executeTaskLoop(taskId, onEvent) {
716
857
  if (onEvent) {
717
858
  onEvent({
718
859
  type: 'task_question',
860
+ source: 'autonomous_task',
719
861
  task_id: taskId,
720
862
  question: classification.questionText,
721
863
  confidence: classification.confidence,
@@ -742,6 +884,7 @@ async function executeTaskLoop(taskId, onEvent) {
742
884
  if (onEvent) {
743
885
  onEvent({
744
886
  type: 'task_stopped',
887
+ source: 'autonomous_task',
745
888
  task_id: taskId,
746
889
  reason: `Error: ${error.message}`,
747
890
  timestamp: Date.now(),
@@ -779,7 +922,7 @@ async function waitForResume(taskId, timeoutMs = WAIT_FOR_RESUME_TIMEOUT_MS) {
779
922
  if (session) {
780
923
  session.status = 'stopped';
781
924
  session.updated_at = Date.now();
782
-
925
+
783
926
  // Remove lock file on timeout
784
927
  removeTaskLock(taskId);
785
928
  }
@@ -797,6 +940,23 @@ async function waitForResume(taskId, timeoutMs = WAIT_FOR_RESUME_TIMEOUT_MS) {
797
940
  });
798
941
  }
799
942
 
943
+ /**
944
+ * Atomically get and clear pending_question from a session
945
+ * Prevents race conditions where the message could be modified between read and clear
946
+ * @param {string} taskId
947
+ * @returns {string|null} The pending question, or null if none exists
948
+ */
949
+ function atomicGetAndClearMessage(taskId) {
950
+ const session = taskSessions.get(taskId);
951
+ if (!session) {
952
+ return null;
953
+ }
954
+
955
+ const message = session.pending_question;
956
+ session.pending_question = null;
957
+ return message;
958
+ }
959
+
800
960
  /**
801
961
  * Stop an task task
802
962
  *
@@ -941,14 +1101,15 @@ export async function answerTaskQuestion(taskId, answer, onEvent) {
941
1101
  return { success: false, reason: 'Session not found' };
942
1102
  }
943
1103
 
944
- if (session.status !== 'waiting_input') {
945
- return { success: false, reason: `Cannot answer question in ${session.status} status` };
1104
+ if (session.status !== 'waiting_input' && session.status !== 'paused') {
1105
+ return { success: false, reason: `Cannot send message in ${session.status} status` };
946
1106
  }
947
1107
 
948
1108
  // Emit user input event
949
1109
  if (onEvent) {
950
1110
  onEvent({
951
1111
  type: 'task_user_input',
1112
+ source: 'autonomous_task',
952
1113
  task_id: taskId,
953
1114
  question: session.pending_question,
954
1115
  answer,
@@ -956,49 +1117,62 @@ export async function answerTaskQuestion(taskId, answer, onEvent) {
956
1117
  });
957
1118
  }
958
1119
 
959
- // Clear pending question and set status to running
960
- session.pending_question = null;
961
- session.status = 'running';
962
- session.updated_at = Date.now();
1120
+ // Handle based on current status
1121
+ if (session.status === 'paused') {
1122
+ // Task is paused waiting for user message (auto_continue disabled)
1123
+ // Set the message as pending_question and resume the execution loop
1124
+ session.pending_question = answer;
1125
+ session.status = 'running';
1126
+ session.updated_at = Date.now();
963
1127
 
964
- // Execute the next turn with the user's answer
965
- const remainingBudget = session.budget_usd - session.cost_usd;
1128
+ // The execution loop will pick up the pending_question and use it as the prompt
1129
+ return { success: true };
1130
+ } else {
1131
+ // Task is waiting_input (Claude asked a question)
1132
+ // Execute immediately with the user's answer
1133
+ session.pending_question = null;
1134
+ session.status = 'running';
1135
+ session.updated_at = Date.now();
966
1136
 
967
- try {
968
- session.turn_count++;
969
- const result = await executeClaudeHeadless({
970
- prompt: answer,
971
- cwd: session.cwd,
972
- resumeSessionId: session.claude_session_id,
973
- budgetUsd: remainingBudget,
974
- taskId,
975
- });
1137
+ const remainingBudget = session.budget_usd - session.cost_usd;
976
1138
 
977
- // Update session
978
- session.claude_session_id = result.session_id || session.claude_session_id;
979
- session.cost_usd += result.cost_usd || 0;
980
- session.updated_at = Date.now();
1139
+ try {
1140
+ session.turn_count++;
1141
+ const result = await executeClaudeHeadless({
1142
+ prompt: answer,
1143
+ cwd: session.cwd,
1144
+ resumeSessionId: session.claude_session_id,
1145
+ budgetUsd: remainingBudget,
1146
+ taskId,
1147
+ parentSessionId: session.teleportation_session_id,
1148
+ });
981
1149
 
982
- // Add to history
983
- session.history.push({
984
- turn: session.turn_count,
985
- prompt: `[User Answer] ${answer}`,
986
- output: result.output?.slice(0, 2000),
987
- cost_usd: result.cost_usd,
988
- success: result.success,
989
- timestamp: Date.now(),
990
- });
1150
+ // Update session
1151
+ session.claude_session_id = result.session_id || session.claude_session_id;
1152
+ session.cost_usd += result.cost_usd || 0;
1153
+ session.updated_at = Date.now();
991
1154
 
992
- // Resume the execution loop
993
- executeTaskLoop(taskId, onEvent).catch(err => {
994
- console.error(`[task] Answer loop error for ${taskId}:`, err.message);
995
- });
1155
+ // Add to history
1156
+ session.history.push({
1157
+ turn: session.turn_count,
1158
+ prompt: `[User Answer] ${answer}`,
1159
+ output: result.output?.slice(0, 2000),
1160
+ cost_usd: result.cost_usd,
1161
+ success: result.success,
1162
+ timestamp: Date.now(),
1163
+ });
996
1164
 
997
- return { success: true };
998
- } catch (error) {
999
- session.status = 'stopped';
1000
- session.updated_at = Date.now();
1001
- return { success: false, reason: error.message };
1165
+ // Resume the execution loop
1166
+ executeTaskLoop(taskId, onEvent).catch(err => {
1167
+ console.error(`[task] Answer loop error for ${taskId}:`, err.message);
1168
+ });
1169
+
1170
+ return { success: true };
1171
+ } catch (error) {
1172
+ session.status = 'stopped';
1173
+ session.updated_at = Date.now();
1174
+ return { success: false, reason: error.message };
1175
+ }
1002
1176
  }
1003
1177
  }
1004
1178