@astroanywhere/agent 0.2.15 → 0.2.17
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/dist/cli.js +10 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +11 -0
- package/dist/commands/start.js.map +1 -1
- package/dist/lib/config.d.ts +9 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +13 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/openclaw-bridge.d.ts.map +1 -1
- package/dist/lib/openclaw-bridge.js +2 -1
- package/dist/lib/openclaw-bridge.js.map +1 -1
- package/dist/lib/pi-rpc.d.ts +137 -0
- package/dist/lib/pi-rpc.d.ts.map +1 -0
- package/dist/lib/pi-rpc.js +204 -0
- package/dist/lib/pi-rpc.js.map +1 -0
- package/dist/lib/providers.d.ts.map +1 -1
- package/dist/lib/providers.js +77 -1
- package/dist/lib/providers.js.map +1 -1
- package/dist/lib/task-executor.d.ts +10 -14
- package/dist/lib/task-executor.d.ts.map +1 -1
- package/dist/lib/task-executor.js +146 -180
- package/dist/lib/task-executor.js.map +1 -1
- package/dist/lib/websocket-client.d.ts +3 -0
- package/dist/lib/websocket-client.d.ts.map +1 -1
- package/dist/lib/websocket-client.js +10 -0
- package/dist/lib/websocket-client.js.map +1 -1
- package/dist/lib/workspace-root.d.ts +37 -0
- package/dist/lib/workspace-root.d.ts.map +1 -0
- package/dist/lib/workspace-root.js +118 -0
- package/dist/lib/workspace-root.js.map +1 -0
- package/dist/lib/worktree.js +17 -2
- package/dist/lib/worktree.js.map +1 -1
- package/dist/providers/base-adapter.d.ts +38 -1
- package/dist/providers/base-adapter.d.ts.map +1 -1
- package/dist/providers/base-adapter.js.map +1 -1
- package/dist/providers/claude-sdk-adapter.d.ts +9 -3
- package/dist/providers/claude-sdk-adapter.d.ts.map +1 -1
- package/dist/providers/claude-sdk-adapter.js +17 -5
- package/dist/providers/claude-sdk-adapter.js.map +1 -1
- package/dist/providers/codex-adapter.d.ts +10 -3
- package/dist/providers/codex-adapter.d.ts.map +1 -1
- package/dist/providers/codex-adapter.js +57 -4
- package/dist/providers/codex-adapter.js.map +1 -1
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +5 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openclaw-adapter.d.ts +10 -3
- package/dist/providers/openclaw-adapter.d.ts.map +1 -1
- package/dist/providers/openclaw-adapter.js +12 -0
- package/dist/providers/openclaw-adapter.js.map +1 -1
- package/dist/providers/opencode-adapter.d.ts +10 -3
- package/dist/providers/opencode-adapter.d.ts.map +1 -1
- package/dist/providers/opencode-adapter.js +12 -0
- package/dist/providers/opencode-adapter.js.map +1 -1
- package/dist/providers/pi-adapter.d.ts +45 -0
- package/dist/providers/pi-adapter.d.ts.map +1 -0
- package/dist/providers/pi-adapter.js +404 -0
- package/dist/providers/pi-adapter.js.map +1 -0
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -43,6 +43,10 @@ export declare class TaskExecutor {
|
|
|
43
43
|
private openclawBridge;
|
|
44
44
|
private tasksByDirectory;
|
|
45
45
|
private pendingSafetyChecks;
|
|
46
|
+
/** Preserved task metadata for worktree recreation on post-completion resume */
|
|
47
|
+
private completedTaskMeta;
|
|
48
|
+
/** TTL for completed task metadata (10 minutes, matches adapter session TTL) */
|
|
49
|
+
private static readonly COMPLETED_TASK_META_TTL_MS;
|
|
46
50
|
constructor(options: TaskExecutorOptions);
|
|
47
51
|
/**
|
|
48
52
|
* Inject the OpenClaw bridge for task execution delegation.
|
|
@@ -92,28 +96,20 @@ export declare class TaskExecutor {
|
|
|
92
96
|
accepted: boolean;
|
|
93
97
|
reason?: string;
|
|
94
98
|
}>;
|
|
95
|
-
/**
|
|
99
|
+
/** Check if an adapter supports session persistence and resume */
|
|
96
100
|
private isResumableAdapter;
|
|
101
|
+
/** Remove completedTaskMeta entries older than TTL */
|
|
102
|
+
private cleanupExpiredTaskMeta;
|
|
97
103
|
/**
|
|
98
104
|
* Find the adapter that has a preserved session for the given task.
|
|
99
|
-
* Supports ClaudeSdkAdapter, CodexAdapter, OpenClawAdapter, and OpenCodeAdapter.
|
|
100
105
|
*/
|
|
101
106
|
private findAdapterWithSession;
|
|
102
107
|
/**
|
|
103
108
|
* Resume a completed task session for post-completion steering.
|
|
104
|
-
*
|
|
109
|
+
* Provider-agnostic — works with any adapter that implements resumeTask().
|
|
110
|
+
* Handles worktree cleanup by falling back to the original project directory.
|
|
105
111
|
*/
|
|
106
|
-
private
|
|
107
|
-
/**
|
|
108
|
-
* Resume a completed Codex task session for post-completion steering.
|
|
109
|
-
* Uses `codex exec resume <threadId>` for multi-turn conversations.
|
|
110
|
-
*/
|
|
111
|
-
private resumeCompletedCodexTask;
|
|
112
|
-
/**
|
|
113
|
-
* Resume a completed OpenClaw or OpenCode session.
|
|
114
|
-
* Generic handler that works for any adapter with resumeTask().
|
|
115
|
-
*/
|
|
116
|
-
private resumeCompletedCliTask;
|
|
112
|
+
private resumeCompletedSession;
|
|
117
113
|
/**
|
|
118
114
|
* Perform safety check on working directory
|
|
119
115
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-executor.d.ts","sourceRoot":"","sources":["../../src/lib/task-executor.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,KAAK,EAAE,IAAI,EAA4B,aAAa,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"task-executor.d.ts","sourceRoot":"","sources":["../../src/lib/task-executor.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,17 +5,16 @@
|
|
|
5
5
|
* and streaming output back over WebSocket
|
|
6
6
|
*/
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
|
-
import { statSync, realpathSync } from 'node:fs';
|
|
8
|
+
import { statSync, realpathSync, existsSync } from 'node:fs';
|
|
9
9
|
import { resolve } from 'node:path';
|
|
10
10
|
import { execFile as execFileCb } from 'node:child_process';
|
|
11
11
|
import { promisify } from 'node:util';
|
|
12
12
|
import { createProviderAdapter } from '../providers/index.js';
|
|
13
13
|
import { ClaudeSdkAdapter } from '../providers/claude-sdk-adapter.js';
|
|
14
|
-
import { CodexAdapter } from '../providers/codex-adapter.js';
|
|
15
14
|
import { OpenClawAdapter } from '../providers/openclaw-adapter.js';
|
|
16
|
-
import { OpenCodeAdapter } from '../providers/opencode-adapter.js';
|
|
17
15
|
import { SlurmJobMonitor } from './slurm-job-monitor.js';
|
|
18
16
|
import { createWorktree, syncProjectWorktree } from './worktree.js';
|
|
17
|
+
import { ensureProjectWorkspace } from './workspace-root.js';
|
|
19
18
|
import { BranchLockManager } from './branch-lock.js';
|
|
20
19
|
import { pushAndCreatePR, mergePullRequest, getRemoteBranchSha, isGhAvailable } from './git-pr.js';
|
|
21
20
|
import { localMergeIntoProjectBranch } from './local-merge.js';
|
|
@@ -159,6 +158,10 @@ export class TaskExecutor {
|
|
|
159
158
|
// Safety tracking
|
|
160
159
|
tasksByDirectory = new Map(); // workdir -> taskIds
|
|
161
160
|
pendingSafetyChecks = new Map(); // taskId -> pending check
|
|
161
|
+
/** Preserved task metadata for worktree recreation on post-completion resume */
|
|
162
|
+
completedTaskMeta = new Map();
|
|
163
|
+
/** TTL for completed task metadata (10 minutes, matches adapter session TTL) */
|
|
164
|
+
static COMPLETED_TASK_META_TTL_MS = 10 * 60 * 1000;
|
|
162
165
|
constructor(options) {
|
|
163
166
|
this.wsClient = options.wsClient;
|
|
164
167
|
this.maxConcurrentTasks = options.maxConcurrentTasks ?? 40;
|
|
@@ -200,29 +203,34 @@ export class TaskExecutor {
|
|
|
200
203
|
async submitTask(task) {
|
|
201
204
|
// Skip workingDirectory resolution for lightweight text-only tasks (no file system access)
|
|
202
205
|
const isTextOnlyTask = task.type === 'summarize' || task.type === 'chat' || task.type === 'plan';
|
|
206
|
+
// Text-only tasks (plan/chat/summarize) can run without a working directory.
|
|
207
|
+
// For all others, resolve the directory or auto-provision one.
|
|
203
208
|
let resolvedWorkDir;
|
|
204
|
-
|
|
205
|
-
resolvedWorkDir =
|
|
206
|
-
? undefined // Text-only tasks can run without a working directory
|
|
207
|
-
: resolveWorkingDirectory(task.workingDirectory);
|
|
209
|
+
if (isTextOnlyTask && !task.workingDirectory) {
|
|
210
|
+
resolvedWorkDir = undefined;
|
|
208
211
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
error: errorMsg
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
212
|
+
else {
|
|
213
|
+
try {
|
|
214
|
+
resolvedWorkDir = resolveWorkingDirectory(task.workingDirectory, task.projectId);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
// Fail fast with a clear error instead of entering the execution pipeline.
|
|
218
|
+
// Without this, tasks with non-existent directories become dead jobs.
|
|
219
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
220
|
+
console.error(`[executor] Task ${task.id}: ${errorMsg}`);
|
|
221
|
+
this.wsClient.sendTaskResult({
|
|
222
|
+
taskId: task.id,
|
|
223
|
+
status: 'failed',
|
|
224
|
+
error: errorMsg,
|
|
225
|
+
completedAt: new Date().toISOString(),
|
|
226
|
+
});
|
|
227
|
+
this.wsClient.removeActiveTask(task.id);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
222
230
|
}
|
|
223
231
|
const normalizedTask = {
|
|
224
232
|
...task,
|
|
225
|
-
workingDirectory: resolvedWorkDir,
|
|
233
|
+
workingDirectory: resolvedWorkDir ?? '',
|
|
226
234
|
};
|
|
227
235
|
// Determine if worktree isolation will be used for this task.
|
|
228
236
|
// Check git availability early: non-git directories cannot use git worktrees,
|
|
@@ -472,8 +480,7 @@ export class TaskExecutor {
|
|
|
472
480
|
const running = this.runningTasks.get(taskId);
|
|
473
481
|
if (running) {
|
|
474
482
|
// Task is still running — inject message into the live session
|
|
475
|
-
|
|
476
|
-
if (running.adapter instanceof ClaudeSdkAdapter && typeof running.adapter.injectMessage === 'function') {
|
|
483
|
+
if (running.adapter.injectMessage) {
|
|
477
484
|
const injected = await running.adapter.injectMessage(taskId, message, interrupt);
|
|
478
485
|
if (injected) {
|
|
479
486
|
return { accepted: true };
|
|
@@ -491,33 +498,27 @@ export class TaskExecutor {
|
|
|
491
498
|
if (sessionId && context.sessionId !== sessionId) {
|
|
492
499
|
console.warn(`[task-executor] Session hint mismatch for task ${taskId}: hint=${sessionId}, actual=${context.sessionId}`);
|
|
493
500
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
return { accepted: true };
|
|
497
|
-
}
|
|
498
|
-
if (resumeAdapter instanceof CodexAdapter) {
|
|
499
|
-
this.resumeCompletedCodexTask(taskId, message, resumeAdapter, context);
|
|
500
|
-
return { accepted: true };
|
|
501
|
-
}
|
|
502
|
-
if (resumeAdapter instanceof OpenClawAdapter || resumeAdapter instanceof OpenCodeAdapter) {
|
|
503
|
-
this.resumeCompletedCliTask(taskId, message, resumeAdapter, context);
|
|
504
|
-
return { accepted: true };
|
|
505
|
-
}
|
|
506
|
-
console.warn(`[task-executor] Adapter has session for task ${taskId} but no resume implementation`);
|
|
501
|
+
this.resumeCompletedSession(taskId, message, resumeAdapter, context);
|
|
502
|
+
return { accepted: true };
|
|
507
503
|
}
|
|
508
504
|
}
|
|
509
505
|
return { accepted: false, reason: 'Task not found or session expired' };
|
|
510
506
|
}
|
|
511
|
-
/**
|
|
507
|
+
/** Check if an adapter supports session persistence and resume */
|
|
512
508
|
isResumableAdapter(adapter) {
|
|
513
|
-
return adapter
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
509
|
+
return typeof adapter.getTaskContext === 'function' && typeof adapter.resumeTask === 'function';
|
|
510
|
+
}
|
|
511
|
+
/** Remove completedTaskMeta entries older than TTL */
|
|
512
|
+
cleanupExpiredTaskMeta() {
|
|
513
|
+
const now = Date.now();
|
|
514
|
+
for (const [key, meta] of this.completedTaskMeta) {
|
|
515
|
+
if (now - meta.storedAt > TaskExecutor.COMPLETED_TASK_META_TTL_MS) {
|
|
516
|
+
this.completedTaskMeta.delete(key);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
517
519
|
}
|
|
518
520
|
/**
|
|
519
521
|
* Find the adapter that has a preserved session for the given task.
|
|
520
|
-
* Supports ClaudeSdkAdapter, CodexAdapter, OpenClawAdapter, and OpenCodeAdapter.
|
|
521
522
|
*/
|
|
522
523
|
findAdapterWithSession(taskId) {
|
|
523
524
|
for (const adapter of this.adapters.values()) {
|
|
@@ -531,9 +532,10 @@ export class TaskExecutor {
|
|
|
531
532
|
}
|
|
532
533
|
/**
|
|
533
534
|
* Resume a completed task session for post-completion steering.
|
|
534
|
-
*
|
|
535
|
+
* Provider-agnostic — works with any adapter that implements resumeTask().
|
|
536
|
+
* Handles worktree cleanup by falling back to the original project directory.
|
|
535
537
|
*/
|
|
536
|
-
async
|
|
538
|
+
async resumeCompletedSession(taskId, message, adapter, context) {
|
|
537
539
|
const abortController = new AbortController();
|
|
538
540
|
let textSequence = 0;
|
|
539
541
|
const stream = {
|
|
@@ -568,129 +570,49 @@ export class TaskExecutor {
|
|
|
568
570
|
return this.wsClient.sendApprovalRequest(taskId, question, options);
|
|
569
571
|
},
|
|
570
572
|
};
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
this.wsClient.sendToolTrace(taskId, toolName, toolInput, toolResult, success);
|
|
611
|
-
},
|
|
612
|
-
text: (data) => {
|
|
613
|
-
this.wsClient.sendTaskText(taskId, data, textSequence++);
|
|
614
|
-
},
|
|
615
|
-
toolUse: (toolName, toolInput) => {
|
|
616
|
-
this.wsClient.sendTaskToolUse(taskId, toolName, toolInput);
|
|
617
|
-
},
|
|
618
|
-
toolResult: (toolName, result, success) => {
|
|
619
|
-
this.wsClient.sendTaskToolResult(taskId, toolName, result, success);
|
|
620
|
-
},
|
|
621
|
-
fileChange: (path, action, linesAdded, linesRemoved, diff) => {
|
|
622
|
-
this.wsClient.sendTaskFileChange(taskId, path, action, linesAdded, linesRemoved, diff);
|
|
623
|
-
},
|
|
624
|
-
sessionInit: (sessionId, model) => {
|
|
625
|
-
this.wsClient.sendTaskSessionInit(taskId, sessionId, model);
|
|
626
|
-
},
|
|
627
|
-
approvalRequest: async (question, options) => {
|
|
628
|
-
return this.wsClient.sendApprovalRequest(taskId, question, options);
|
|
629
|
-
},
|
|
630
|
-
};
|
|
631
|
-
try {
|
|
632
|
-
this.wsClient.sendTaskStatus(taskId, 'running', 0, 'Resuming Codex session...');
|
|
633
|
-
const result = await adapter.resumeTask(taskId, message, context.workingDirectory ?? process.cwd(), context.sessionId, stream, abortController.signal);
|
|
634
|
-
this.wsClient.sendTaskResult({
|
|
635
|
-
taskId,
|
|
636
|
-
status: result.success ? 'completed' : 'failed',
|
|
637
|
-
output: result.output,
|
|
638
|
-
error: result.error,
|
|
639
|
-
completedAt: new Date().toISOString(),
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
catch (error) {
|
|
643
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
644
|
-
this.wsClient.sendTaskResult({
|
|
645
|
-
taskId,
|
|
646
|
-
status: 'failed',
|
|
647
|
-
error: `Codex resume failed: ${errorMsg}`,
|
|
648
|
-
completedAt: new Date().toISOString(),
|
|
649
|
-
});
|
|
573
|
+
// Resolve working directory: existing worktree → recreate worktree → project dir → cwd
|
|
574
|
+
let resumeDir = context.workingDirectory;
|
|
575
|
+
let resumeWorktreeCleanup;
|
|
576
|
+
if (resumeDir && !existsSync(resumeDir)) {
|
|
577
|
+
const meta = this.completedTaskMeta.get(taskId);
|
|
578
|
+
if (meta?.originalWorkingDirectory && existsSync(meta.originalWorkingDirectory)) {
|
|
579
|
+
// Try to recreate a worktree from the project branch (like a new task)
|
|
580
|
+
try {
|
|
581
|
+
console.log(`[executor] Task ${taskId}: worktree gone, recreating from project branch at ${meta.originalWorkingDirectory}`);
|
|
582
|
+
const worktree = await createWorktree({
|
|
583
|
+
workingDirectory: meta.originalWorkingDirectory,
|
|
584
|
+
taskId: `resume-${taskId.slice(0, 12)}-${Date.now()}`,
|
|
585
|
+
projectId: meta.projectId,
|
|
586
|
+
shortProjectId: meta.shortProjectId,
|
|
587
|
+
shortNodeId: meta.shortNodeId,
|
|
588
|
+
agentDir: meta.agentDir,
|
|
589
|
+
baseBranch: meta.baseBranch,
|
|
590
|
+
projectBranch: meta.projectBranch,
|
|
591
|
+
stdout: stream.stdout,
|
|
592
|
+
stderr: stream.stderr,
|
|
593
|
+
});
|
|
594
|
+
if (worktree) {
|
|
595
|
+
resumeDir = worktree.workingDirectory;
|
|
596
|
+
resumeWorktreeCleanup = () => worktree.cleanup();
|
|
597
|
+
console.log(`[executor] Task ${taskId}: resume worktree created at ${resumeDir}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
catch (err) {
|
|
601
|
+
console.warn(`[executor] Task ${taskId}: worktree recreation failed (${err instanceof Error ? err.message : err}), using project dir`);
|
|
602
|
+
}
|
|
603
|
+
// If worktree creation failed, fall back to project dir
|
|
604
|
+
if (!resumeWorktreeCleanup) {
|
|
605
|
+
resumeDir = meta.originalWorkingDirectory;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
console.warn(`[executor] Task ${taskId}: worktree gone and no project dir available, using process.cwd()`);
|
|
610
|
+
resumeDir = process.cwd();
|
|
611
|
+
}
|
|
650
612
|
}
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
|
-
* Resume a completed OpenClaw or OpenCode session.
|
|
654
|
-
* Generic handler that works for any adapter with resumeTask().
|
|
655
|
-
*/
|
|
656
|
-
async resumeCompletedCliTask(taskId, message, adapter, context) {
|
|
657
|
-
const abortController = new AbortController();
|
|
658
|
-
let textSequence = 0;
|
|
659
|
-
const stream = {
|
|
660
|
-
stdout: (data) => {
|
|
661
|
-
this.wsClient.sendTaskOutput(taskId, 'stdout', data, 0);
|
|
662
|
-
},
|
|
663
|
-
stderr: (data) => {
|
|
664
|
-
this.wsClient.sendTaskOutput(taskId, 'stderr', data, 0);
|
|
665
|
-
},
|
|
666
|
-
status: (status, progress, statusMessage) => {
|
|
667
|
-
this.wsClient.sendTaskStatus(taskId, status, progress, statusMessage);
|
|
668
|
-
},
|
|
669
|
-
toolTrace: (toolName, toolInput, toolResult, success) => {
|
|
670
|
-
this.wsClient.sendToolTrace(taskId, toolName, toolInput, toolResult, success);
|
|
671
|
-
},
|
|
672
|
-
text: (data) => {
|
|
673
|
-
this.wsClient.sendTaskText(taskId, data, textSequence++);
|
|
674
|
-
},
|
|
675
|
-
toolUse: (toolName, toolInput) => {
|
|
676
|
-
this.wsClient.sendTaskToolUse(taskId, toolName, toolInput);
|
|
677
|
-
},
|
|
678
|
-
toolResult: (toolName, result, success) => {
|
|
679
|
-
this.wsClient.sendTaskToolResult(taskId, toolName, result, success);
|
|
680
|
-
},
|
|
681
|
-
fileChange: (path, action, linesAdded, linesRemoved, diff) => {
|
|
682
|
-
this.wsClient.sendTaskFileChange(taskId, path, action, linesAdded, linesRemoved, diff);
|
|
683
|
-
},
|
|
684
|
-
sessionInit: (sessionId, model) => {
|
|
685
|
-
this.wsClient.sendTaskSessionInit(taskId, sessionId, model);
|
|
686
|
-
},
|
|
687
|
-
approvalRequest: async (question, options) => {
|
|
688
|
-
return this.wsClient.sendApprovalRequest(taskId, question, options);
|
|
689
|
-
},
|
|
690
|
-
};
|
|
691
613
|
try {
|
|
692
614
|
this.wsClient.sendTaskStatus(taskId, 'running', 0, `Resuming ${adapter.name} session...`);
|
|
693
|
-
const result = await adapter.resumeTask(taskId, message,
|
|
615
|
+
const result = await adapter.resumeTask(taskId, message, resumeDir ?? process.cwd(), context.sessionId, stream, abortController.signal);
|
|
694
616
|
this.wsClient.sendTaskResult({
|
|
695
617
|
taskId,
|
|
696
618
|
status: result.success ? 'completed' : 'failed',
|
|
@@ -704,10 +626,22 @@ export class TaskExecutor {
|
|
|
704
626
|
this.wsClient.sendTaskResult({
|
|
705
627
|
taskId,
|
|
706
628
|
status: 'failed',
|
|
707
|
-
error:
|
|
629
|
+
error: `Resume failed: ${errorMsg}`,
|
|
708
630
|
completedAt: new Date().toISOString(),
|
|
709
631
|
});
|
|
710
632
|
}
|
|
633
|
+
finally {
|
|
634
|
+
// Clean up the resume worktree (if one was created)
|
|
635
|
+
if (resumeWorktreeCleanup) {
|
|
636
|
+
try {
|
|
637
|
+
await resumeWorktreeCleanup();
|
|
638
|
+
console.log(`[executor] Task ${taskId}: resume worktree cleaned up`);
|
|
639
|
+
}
|
|
640
|
+
catch (err) {
|
|
641
|
+
console.warn(`[executor] Task ${taskId}: resume worktree cleanup failed:`, err);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
711
645
|
}
|
|
712
646
|
// ============================================================================
|
|
713
647
|
// Private Methods
|
|
@@ -913,7 +847,7 @@ export class TaskExecutor {
|
|
|
913
847
|
this.processQueue();
|
|
914
848
|
return;
|
|
915
849
|
}
|
|
916
|
-
console.log(`[executor] Task ${task.id}: adapter ${adapter.name} (${adapter.type}) ready`);
|
|
850
|
+
console.log(`[executor] Task ${task.id}: adapter ${adapter.name} (${adapter.type}) ready, taskType=${normalizedTask.type ?? 'undefined'}, provider=${normalizedTask.provider}, resumeSessionId=${normalizedTask.resumeSessionId ?? 'none'}`);
|
|
917
851
|
const abortController = new AbortController();
|
|
918
852
|
const outputSequence = { stdout: 0, stderr: 0 };
|
|
919
853
|
// Create stream handlers before workspace prep so setup output is captured
|
|
@@ -1025,23 +959,31 @@ export class TaskExecutor {
|
|
|
1025
959
|
&& isTextOnly;
|
|
1026
960
|
let result;
|
|
1027
961
|
if (canResume) {
|
|
1028
|
-
console.log(`[executor] Task ${task.id}: resuming session ${taskWithWorkspace.resumeSessionId} with ${adapter.name}...`);
|
|
962
|
+
console.log(`[executor] Task ${task.id}: resuming session ${taskWithWorkspace.resumeSessionId} with ${adapter.name} (type=${adapter.type}, hasMessages=${!!taskWithWorkspace.messages?.length})...`);
|
|
1029
963
|
const resumeStartedAt = new Date().toISOString();
|
|
1030
964
|
try {
|
|
1031
965
|
const resumeResult = await adapter.resumeTask(taskWithWorkspace.id, taskWithWorkspace.prompt, taskWithWorkspace.workingDirectory || process.cwd(), taskWithWorkspace.resumeSessionId, stream, abortController.signal);
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
966
|
+
if (resumeResult.success) {
|
|
967
|
+
result = {
|
|
968
|
+
taskId: taskWithWorkspace.id,
|
|
969
|
+
status: 'completed',
|
|
970
|
+
output: resumeResult.output,
|
|
971
|
+
startedAt: resumeStartedAt,
|
|
972
|
+
completedAt: new Date().toISOString(),
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
else {
|
|
976
|
+
// Resume resolved but failed (e.g. Codex session expired, CLI error) —
|
|
977
|
+
// fall back to fresh execution. Codex/OpenCode adapters resolve with
|
|
978
|
+
// { success: false } instead of throwing, so the catch block won't fire.
|
|
979
|
+
console.warn(`[executor] Task ${task.id}: resume returned failure (${resumeResult.error}), falling back to fresh execution`);
|
|
980
|
+
result = await adapter.execute(taskWithWorkspace, stream, abortController.signal);
|
|
981
|
+
}
|
|
1040
982
|
}
|
|
1041
983
|
catch (resumeErr) {
|
|
1042
|
-
// Resume
|
|
984
|
+
// Resume threw (Claude SDK throws on session errors) —
|
|
1043
985
|
// fall back to fresh execution with conversation history in messages
|
|
1044
|
-
console.warn(`[executor] Task ${task.id}: resume
|
|
986
|
+
console.warn(`[executor] Task ${task.id}: resume threw (${resumeErr instanceof Error ? resumeErr.message : resumeErr}), falling back to fresh execution`);
|
|
1045
987
|
result = await adapter.execute(taskWithWorkspace, stream, abortController.signal);
|
|
1046
988
|
}
|
|
1047
989
|
}
|
|
@@ -1049,6 +991,23 @@ export class TaskExecutor {
|
|
|
1049
991
|
console.log(`[executor] Task ${task.id}: executing with ${adapter.name}...`);
|
|
1050
992
|
result = await adapter.execute(taskWithWorkspace, stream, abortController.signal);
|
|
1051
993
|
}
|
|
994
|
+
// Store original working directory on the adapter's session so post-completion
|
|
995
|
+
// resume can fall back to it when the worktree has been cleaned up.
|
|
996
|
+
if (normalizedTask.workingDirectory && prepared.workingDirectory !== normalizedTask.workingDirectory) {
|
|
997
|
+
adapter.setOriginalWorkingDirectory?.(task.id, normalizedTask.workingDirectory);
|
|
998
|
+
// Preserve task metadata for worktree recreation on resume
|
|
999
|
+
this.cleanupExpiredTaskMeta();
|
|
1000
|
+
this.completedTaskMeta.set(task.id, {
|
|
1001
|
+
originalWorkingDirectory: normalizedTask.workingDirectory,
|
|
1002
|
+
projectBranch: normalizedTask.projectBranch,
|
|
1003
|
+
baseBranch: normalizedTask.baseBranch,
|
|
1004
|
+
agentDir: normalizedTask.agentDir,
|
|
1005
|
+
shortProjectId: normalizedTask.shortProjectId,
|
|
1006
|
+
shortNodeId: normalizedTask.shortNodeId,
|
|
1007
|
+
projectId: normalizedTask.projectId,
|
|
1008
|
+
storedAt: Date.now(),
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1052
1011
|
// Emit accurate file change events via git diff (covers all change methods)
|
|
1053
1012
|
if (!isTextOnly && prepared.workingDirectory) {
|
|
1054
1013
|
await this.emitGitDiffFileChanges(task.id, prepared.workingDirectory, prepared.commitBeforeSha, stream);
|
|
@@ -1068,7 +1027,7 @@ export class TaskExecutor {
|
|
|
1068
1027
|
if (summary) {
|
|
1069
1028
|
console.log(`[executor] Task ${task.id}: summary available — status=${summary.status}, workCompleted=${!!summary.workCompleted}, executiveSummary=${!!summary.executiveSummary}, keyFindings=${summary.keyFindings?.length ?? 0}, filesChanged=${summary.filesChanged?.length ?? 0}`);
|
|
1070
1029
|
}
|
|
1071
|
-
else {
|
|
1030
|
+
else if (!isTextOnly) {
|
|
1072
1031
|
console.warn(`[executor] Task ${task.id}: no summary available for PR body`);
|
|
1073
1032
|
}
|
|
1074
1033
|
const rawTitle = summary?.workCompleted || task.title || task.prompt.slice(0, 100);
|
|
@@ -1698,12 +1657,19 @@ export class TaskExecutor {
|
|
|
1698
1657
|
}
|
|
1699
1658
|
/**
|
|
1700
1659
|
* Resolve a working directory value.
|
|
1701
|
-
* - Empty/missing
|
|
1660
|
+
* - Empty/missing + projectId → auto-provision workspace under workspace root
|
|
1661
|
+
* - Empty/missing + no projectId → error
|
|
1702
1662
|
* - Git URL → error (repo setup should have resolved this to a local path)
|
|
1703
1663
|
* - Otherwise → return as-is
|
|
1704
1664
|
*/
|
|
1705
|
-
function resolveWorkingDirectory(value) {
|
|
1665
|
+
function resolveWorkingDirectory(value, projectId) {
|
|
1706
1666
|
if (!value) {
|
|
1667
|
+
if (projectId) {
|
|
1668
|
+
// Auto-provision a temporary workspace for this project (no git init)
|
|
1669
|
+
const dir = ensureProjectWorkspace(projectId);
|
|
1670
|
+
console.log(`[executor] Auto-provisioned workspace for project ${projectId}: ${dir}`);
|
|
1671
|
+
return dir;
|
|
1672
|
+
}
|
|
1707
1673
|
throw new Error('workingDirectory is required but was not provided. ' +
|
|
1708
1674
|
'Configure a project directory or repository before dispatching tasks.');
|
|
1709
1675
|
}
|