teleportation-cli 1.1.5 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/permission_request.mjs +326 -59
- package/.claude/hooks/post_tool_use.mjs +90 -0
- package/.claude/hooks/pre_tool_use.mjs +212 -293
- package/.claude/hooks/session-register.mjs +89 -104
- package/.claude/hooks/session_end.mjs +41 -42
- package/.claude/hooks/session_start.mjs +45 -60
- package/.claude/hooks/stop.mjs +752 -99
- package/.claude/hooks/user_prompt_submit.mjs +26 -3
- package/lib/cli/daemon-commands.js +1 -1
- package/lib/cli/teleport-commands.js +469 -0
- package/lib/daemon/daemon-v2.js +104 -0
- package/lib/daemon/lifecycle.js +56 -171
- package/lib/daemon/services/index.js +3 -0
- package/lib/daemon/services/polling-service.js +173 -0
- package/lib/daemon/services/queue-service.js +318 -0
- package/lib/daemon/services/session-service.js +115 -0
- package/lib/daemon/state.js +35 -0
- package/lib/daemon/task-executor-v2.js +413 -0
- package/lib/daemon/task-executor.js +270 -96
- package/lib/daemon/teleportation-daemon.js +709 -126
- package/lib/daemon/timeline-analyzer.js +215 -0
- package/lib/daemon/transcript-ingestion.js +696 -0
- package/lib/daemon/utils.js +91 -0
- package/lib/install/installer.js +184 -20
- package/lib/install/uhr-installer.js +136 -0
- package/lib/remote/providers/base-provider.js +46 -0
- package/lib/remote/providers/daytona-provider.js +58 -0
- package/lib/remote/providers/provider-factory.js +90 -19
- package/lib/remote/providers/sprites-provider.js +711 -0
- package/lib/teleport/exporters/claude-exporter.js +302 -0
- package/lib/teleport/exporters/gemini-exporter.js +307 -0
- package/lib/teleport/exporters/index.js +93 -0
- package/lib/teleport/exporters/interface.js +153 -0
- package/lib/teleport/fork-tracker.js +415 -0
- package/lib/teleport/git-committer.js +337 -0
- package/lib/teleport/index.js +48 -0
- package/lib/teleport/manager.js +620 -0
- package/lib/teleport/session-capture.js +282 -0
- package/package.json +9 -5
- package/teleportation-cli.cjs +488 -453
- package/.claude/hooks/heartbeat.mjs +0 -396
- package/lib/daemon/pid-manager.js +0 -183
|
@@ -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
|
-
* -
|
|
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
|
-
//
|
|
261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
960
|
-
session.
|
|
961
|
-
|
|
962
|
-
|
|
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
|
-
|
|
965
|
-
|
|
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
|
-
|
|
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
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
|