@saadnvd1/agent-os 0.3.824 → 0.3.825
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/app/api/fork/cancel/route.ts +19 -0
- package/app/api/fork/children/route.ts +52 -0
- package/app/api/fork/result/route.ts +29 -0
- package/app/api/fork/return/route.ts +20 -0
- package/app/api/fork/route.ts +32 -0
- package/app/api/fork/wait/route.ts +27 -0
- package/app/api/load/heavy/lease/route.ts +26 -0
- package/app/api/sessions/[id]/archive/route.ts +17 -4
- package/app/api/sessions/[id]/route.ts +2 -3
- package/app/page.tsx +0 -5
- package/components/Chat/ChatPanel.tsx +33 -206
- package/components/Chat/Forks.tsx +278 -0
- package/components/Chat/ReplyingTo.tsx +24 -0
- package/components/Chat/TimelineRows.tsx +194 -0
- package/components/Pane/DesktopTabBar.tsx +0 -6
- package/components/Pane/MobileTabBar.tsx +0 -6
- package/components/Pane/MobileViewPicker.test.ts +1 -8
- package/components/Pane/MobileViewPicker.tsx +0 -11
- package/components/Pane/PaneViewToggle.test.ts +0 -2
- package/components/Pane/PaneViewToggle.tsx +0 -17
- package/components/Pane/index.tsx +0 -50
- package/components/SessionList/useDoneAction.ts +18 -3
- package/contexts/PaneContext.tsx +1 -1
- package/data/done/index.ts +26 -2
- package/data/fork/index.ts +83 -0
- package/lib/chat/commands.ts +15 -0
- package/lib/chat/runner.ts +8 -2
- package/lib/chat/worker/extras.ts +7 -1
- package/lib/chat/worker/host.ts +2 -0
- package/lib/config/client-config-types.ts +7 -0
- package/lib/config/client-config.ts +2 -0
- package/lib/db/migrations/20261102T0100_forks.ts +87 -0
- package/lib/done/archive.ts +21 -1
- package/lib/experiments/registry.ts +8 -0
- package/lib/fork/api.ts +10 -0
- package/lib/fork/brief.ts +63 -0
- package/lib/fork/conversation.ts +75 -0
- package/lib/fork/deliver.ts +91 -0
- package/lib/fork/extras.ts +53 -0
- package/lib/fork/guard.ts +323 -0
- package/lib/fork/heavy-lease.ts +104 -0
- package/lib/fork/held.ts +26 -0
- package/lib/fork/ledger.ts +324 -0
- package/lib/fork/options.ts +46 -0
- package/lib/fork/point.ts +41 -0
- package/lib/fork/scratch.ts +67 -0
- package/lib/fork/settle.ts +215 -0
- package/lib/fork/snapshot.ts +69 -0
- package/lib/fork/start.ts +273 -0
- package/lib/fork/tick.ts +54 -0
- package/lib/fork/types.ts +79 -0
- package/lib/fork/view.ts +116 -0
- package/lib/fork/waits.ts +83 -0
- package/lib/fork/worktree.ts +115 -0
- package/lib/orchestrator/shell-guard.ts +6 -3
- package/lib/orchestrator/write-guard.ts +1 -1
- package/lib/security/demo.ts +1 -2
- package/lib/sessions/end.ts +5 -0
- package/package.json +1 -1
- package/server.ts +2 -0
- package/app/api/orchestrate/spawn/route.ts +0 -54
- package/app/api/orchestrate/workers/[id]/route.ts +0 -89
- package/app/api/orchestrate/workers/route.ts +0 -31
- package/app/api/sessions/[id]/mcp-config/route.ts +0 -34
- package/components/ConductorPanel.tsx +0 -258
- package/lib/mcp-config.ts +0 -80
- package/lib/orchestration.ts +0 -463
- package/mcp/orchestration-server.ts +0 -441
package/lib/mcp-config.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP Config Auto-Generation
|
|
3
|
-
*
|
|
4
|
-
* Writes a .mcp.json file to the session's working directory so Claude
|
|
5
|
-
* automatically picks up the orchestration tools with the session ID baked in.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { writeFileSync, existsSync, readFileSync } from "fs";
|
|
9
|
-
import path from "path";
|
|
10
|
-
import { selfUrl } from "./agents/self-env";
|
|
11
|
-
|
|
12
|
-
interface McpConfig {
|
|
13
|
-
mcpServers: Record<
|
|
14
|
-
string,
|
|
15
|
-
{
|
|
16
|
-
command: string;
|
|
17
|
-
args: string[];
|
|
18
|
-
env?: Record<string, string>;
|
|
19
|
-
}
|
|
20
|
-
>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Write or update .mcp.json in the working directory with orchestration server config
|
|
25
|
-
*/
|
|
26
|
-
export function ensureMcpConfig(
|
|
27
|
-
workingDirectory: string,
|
|
28
|
-
sessionId: string
|
|
29
|
-
): void {
|
|
30
|
-
const configPath = path.join(workingDirectory, ".mcp.json");
|
|
31
|
-
const orchestrationServerPath = path.join(
|
|
32
|
-
process.cwd(),
|
|
33
|
-
"mcp",
|
|
34
|
-
"orchestration-server.ts"
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
let config: McpConfig = { mcpServers: {} };
|
|
38
|
-
|
|
39
|
-
// Read existing config if present
|
|
40
|
-
if (existsSync(configPath)) {
|
|
41
|
-
try {
|
|
42
|
-
const existing = readFileSync(configPath, "utf-8");
|
|
43
|
-
config = JSON.parse(existing);
|
|
44
|
-
if (!config.mcpServers) {
|
|
45
|
-
config.mcpServers = {};
|
|
46
|
-
}
|
|
47
|
-
} catch {
|
|
48
|
-
// Invalid JSON, start fresh
|
|
49
|
-
config = { mcpServers: {} };
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Add/update agent-os orchestration server
|
|
54
|
-
config.mcpServers["agent-os"] = {
|
|
55
|
-
command: "npx",
|
|
56
|
-
args: ["tsx", orchestrationServerPath],
|
|
57
|
-
env: {
|
|
58
|
-
AGENTOS_URL: selfUrl(),
|
|
59
|
-
CONDUCTOR_SESSION_ID: sessionId,
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// Write config
|
|
64
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Check if .mcp.json exists and has agent-os configured
|
|
69
|
-
*/
|
|
70
|
-
export function hasMcpConfig(workingDirectory: string): boolean {
|
|
71
|
-
const configPath = path.join(workingDirectory, ".mcp.json");
|
|
72
|
-
if (!existsSync(configPath)) return false;
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
76
|
-
return !!config.mcpServers?.["agent-os"];
|
|
77
|
-
} catch {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
}
|
package/lib/orchestration.ts
DELETED
|
@@ -1,463 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Orchestration System
|
|
3
|
-
*
|
|
4
|
-
* Allows a "conductor" session to spawn and manage worker sessions.
|
|
5
|
-
* Each worker gets its own git worktree for isolation.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { randomUUID } from "crypto";
|
|
9
|
-
import { rm } from "fs/promises";
|
|
10
|
-
import { homedir } from "os";
|
|
11
|
-
import { db, queries, type Session } from "./db";
|
|
12
|
-
import { createWorktree, deleteWorktree, mainCheckoutOf } from "./worktrees";
|
|
13
|
-
import { setupWorktree } from "./env-setup";
|
|
14
|
-
import { resolveModelForAgent } from "./model-catalog";
|
|
15
|
-
import { type AgentType, getProvider } from "./providers";
|
|
16
|
-
import { statusDetector } from "./status-detector";
|
|
17
|
-
import { wrapWithBanner } from "./banner";
|
|
18
|
-
import { UNSET_SERVER_ENV } from "./agents/server-env";
|
|
19
|
-
import { runInBackground } from "./async-operations";
|
|
20
|
-
import { tmux } from "./tmux/exec";
|
|
21
|
-
import { endTmuxSession } from "./tmux/end";
|
|
22
|
-
import { inheritGroup } from "@/lib/session-groups/assignSessions";
|
|
23
|
-
|
|
24
|
-
export interface SpawnWorkerOptions {
|
|
25
|
-
conductorSessionId: string;
|
|
26
|
-
task: string;
|
|
27
|
-
workingDirectory: string;
|
|
28
|
-
branchName?: string;
|
|
29
|
-
useWorktree?: boolean;
|
|
30
|
-
model?: string;
|
|
31
|
-
agentType?: AgentType;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface WorkerInfo {
|
|
35
|
-
id: string;
|
|
36
|
-
name: string;
|
|
37
|
-
task: string;
|
|
38
|
-
status:
|
|
39
|
-
| "pending"
|
|
40
|
-
| "running"
|
|
41
|
-
| "waiting"
|
|
42
|
-
| "idle"
|
|
43
|
-
| "completed"
|
|
44
|
-
| "failed"
|
|
45
|
-
| "dead";
|
|
46
|
-
worktreePath: string | null;
|
|
47
|
-
branchName: string | null;
|
|
48
|
-
createdAt: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Generate a unique branch name from a task description
|
|
53
|
-
*/
|
|
54
|
-
function taskToBranchName(task: string): string {
|
|
55
|
-
const base =
|
|
56
|
-
task
|
|
57
|
-
.toLowerCase()
|
|
58
|
-
.replace(/[^a-z0-9\s]/g, "")
|
|
59
|
-
.split(/\s+/)
|
|
60
|
-
.slice(0, 4)
|
|
61
|
-
.join("-")
|
|
62
|
-
.slice(0, 30) || "worker";
|
|
63
|
-
|
|
64
|
-
// Add short unique suffix to avoid conflicts
|
|
65
|
-
const suffix = Date.now().toString(36).slice(-4);
|
|
66
|
-
return `${base}-${suffix}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Generate a short session name from a task description
|
|
71
|
-
*/
|
|
72
|
-
function taskToSessionName(task: string): string {
|
|
73
|
-
// Take first 50 chars, trim to last complete word
|
|
74
|
-
const truncated = task.slice(0, 50);
|
|
75
|
-
const lastSpace = truncated.lastIndexOf(" ");
|
|
76
|
-
return lastSpace > 20 ? truncated.slice(0, lastSpace) : truncated;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Spawn a new worker session
|
|
81
|
-
*/
|
|
82
|
-
export async function spawnWorker(
|
|
83
|
-
options: SpawnWorkerOptions
|
|
84
|
-
): Promise<Session> {
|
|
85
|
-
const {
|
|
86
|
-
conductorSessionId,
|
|
87
|
-
task,
|
|
88
|
-
workingDirectory: rawWorkingDir,
|
|
89
|
-
branchName = taskToBranchName(task),
|
|
90
|
-
useWorktree = true,
|
|
91
|
-
agentType = "claude",
|
|
92
|
-
} = options;
|
|
93
|
-
const model = resolveModelForAgent(agentType, options.model);
|
|
94
|
-
|
|
95
|
-
// Expand ~ to home directory
|
|
96
|
-
const workingDirectory = rawWorkingDir.replace(/^~(?=\/|$)/, homedir());
|
|
97
|
-
|
|
98
|
-
const sessionId = randomUUID();
|
|
99
|
-
const sessionName = taskToSessionName(task);
|
|
100
|
-
const provider = getProvider(agentType);
|
|
101
|
-
|
|
102
|
-
let worktreePath: string | null = null;
|
|
103
|
-
let actualWorkingDir = workingDirectory;
|
|
104
|
-
|
|
105
|
-
// Create worktree if requested
|
|
106
|
-
if (useWorktree) {
|
|
107
|
-
try {
|
|
108
|
-
const worktreeResult = await createWorktree({
|
|
109
|
-
projectPath: workingDirectory,
|
|
110
|
-
featureName: branchName,
|
|
111
|
-
});
|
|
112
|
-
worktreePath = worktreeResult.worktreePath;
|
|
113
|
-
actualWorkingDir = worktreePath;
|
|
114
|
-
|
|
115
|
-
// Set up environment in background (copy .env files, install deps)
|
|
116
|
-
const capturedWorktreePath = worktreePath;
|
|
117
|
-
const capturedSourcePath = workingDirectory;
|
|
118
|
-
runInBackground(async () => {
|
|
119
|
-
const result = await setupWorktree({
|
|
120
|
-
worktreePath: capturedWorktreePath,
|
|
121
|
-
sourcePath: capturedSourcePath,
|
|
122
|
-
});
|
|
123
|
-
console.log("Worker worktree setup completed:", {
|
|
124
|
-
worktreePath: capturedWorktreePath,
|
|
125
|
-
envFilesCopied: result.envFilesCopied,
|
|
126
|
-
stepsRun: result.steps.length,
|
|
127
|
-
success: result.success,
|
|
128
|
-
});
|
|
129
|
-
}, `setup-worker-worktree-${sessionId}`);
|
|
130
|
-
} catch (error) {
|
|
131
|
-
console.error("Failed to create worktree:", error);
|
|
132
|
-
// Fall back to same directory (no isolation)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Create session in database
|
|
137
|
-
const tmuxName = `${provider.id}-${sessionId}`;
|
|
138
|
-
queries.createWorkerSession(db).run(
|
|
139
|
-
sessionId,
|
|
140
|
-
sessionName,
|
|
141
|
-
tmuxName,
|
|
142
|
-
actualWorkingDir,
|
|
143
|
-
conductorSessionId,
|
|
144
|
-
task,
|
|
145
|
-
model,
|
|
146
|
-
"sessions", // group_path
|
|
147
|
-
agentType,
|
|
148
|
-
"uncategorized" // project_id
|
|
149
|
-
);
|
|
150
|
-
inheritGroup(conductorSessionId, sessionId);
|
|
151
|
-
|
|
152
|
-
// Update worktree info if created
|
|
153
|
-
if (worktreePath) {
|
|
154
|
-
queries.updateSessionWorktree(db).run(
|
|
155
|
-
worktreePath,
|
|
156
|
-
branchName,
|
|
157
|
-
"main", // base_branch
|
|
158
|
-
null, // dev_server_port
|
|
159
|
-
sessionId
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Create tmux session and start the agent
|
|
164
|
-
const tmuxSessionName = `${provider.id}-${sessionId}`;
|
|
165
|
-
const cwd = actualWorkingDir;
|
|
166
|
-
const target = `=${tmuxSessionName}:`;
|
|
167
|
-
|
|
168
|
-
// Build the initial prompt command (workers use auto-approve by default for automation)
|
|
169
|
-
const flags = provider.buildFlags({ model, autoApprove: true });
|
|
170
|
-
const flagsStr = flags.join(" ");
|
|
171
|
-
|
|
172
|
-
// Create tmux session with the agent and banner
|
|
173
|
-
const agentCmd = `${provider.command} ${flagsStr}`;
|
|
174
|
-
const newSessionCmd =
|
|
175
|
-
UNSET_SERVER_ENV + wrapWithBanner(agentCmd, provider.preamble);
|
|
176
|
-
try {
|
|
177
|
-
await tmux(["set", "-g", "mouse", "on"]).catch(() => {});
|
|
178
|
-
// The last argument is the agent's command line, which tmux runs.
|
|
179
|
-
await tmux([
|
|
180
|
-
"new-session",
|
|
181
|
-
"-d",
|
|
182
|
-
"-s",
|
|
183
|
-
tmuxSessionName,
|
|
184
|
-
"-c",
|
|
185
|
-
cwd,
|
|
186
|
-
"-e",
|
|
187
|
-
`AGENTOS_SESSION_ID=${sessionId}`,
|
|
188
|
-
newSessionCmd,
|
|
189
|
-
]);
|
|
190
|
-
|
|
191
|
-
// Wait for Claude to be ready by checking for the input prompt
|
|
192
|
-
// Poll every 2 seconds for up to 30 seconds
|
|
193
|
-
const maxWaitMs = 30000;
|
|
194
|
-
const pollIntervalMs = 2000;
|
|
195
|
-
let waited = 0;
|
|
196
|
-
let ready = false;
|
|
197
|
-
|
|
198
|
-
console.log(
|
|
199
|
-
`[orchestration] Waiting for Claude to initialize in ${tmuxSessionName}...`
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
while (waited < maxWaitMs && !ready) {
|
|
203
|
-
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
204
|
-
waited += pollIntervalMs;
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
const stdout = await tmux([
|
|
208
|
-
"capture-pane",
|
|
209
|
-
"-t",
|
|
210
|
-
target,
|
|
211
|
-
"-p",
|
|
212
|
-
"-S",
|
|
213
|
-
"-10",
|
|
214
|
-
]);
|
|
215
|
-
const content = stdout.toLowerCase();
|
|
216
|
-
|
|
217
|
-
// Check for trust/permissions prompt and auto-accept
|
|
218
|
-
// Claude shows "Ready to code here?" with "Yes, continue" option - just press Enter
|
|
219
|
-
if (
|
|
220
|
-
content.includes("ready to code here") ||
|
|
221
|
-
content.includes("yes, continue") ||
|
|
222
|
-
content.includes("need permission to work")
|
|
223
|
-
) {
|
|
224
|
-
console.log(
|
|
225
|
-
`[orchestration] Trust prompt detected, pressing Enter to accept`
|
|
226
|
-
);
|
|
227
|
-
await tmux(["send-keys", "-t", target, "Enter"]);
|
|
228
|
-
continue; // Keep waiting for the real prompt
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Look for Claude's ready state - the "? for shortcuts" line indicates fully loaded
|
|
232
|
-
const lines = stdout.trim().split("\n");
|
|
233
|
-
const lastFewLines = lines.slice(-3).join("\n");
|
|
234
|
-
if (
|
|
235
|
-
lastFewLines.includes("? for shortcuts") ||
|
|
236
|
-
lastFewLines.includes("?>")
|
|
237
|
-
) {
|
|
238
|
-
ready = true;
|
|
239
|
-
console.log(`[orchestration] Claude ready after ${waited}ms`);
|
|
240
|
-
}
|
|
241
|
-
} catch {
|
|
242
|
-
// Session might not be ready yet
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (!ready) {
|
|
247
|
-
console.log(
|
|
248
|
-
`[orchestration] Timed out waiting for Claude, sending task anyway after ${waited}ms`
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Send the task as input, then press Enter
|
|
253
|
-
console.log(
|
|
254
|
-
`[orchestration] Sending task to ${tmuxSessionName}: "${task}"`
|
|
255
|
-
);
|
|
256
|
-
try {
|
|
257
|
-
await tmux(["send-keys", "-t", target, "-l", "--", task]);
|
|
258
|
-
await tmux(["send-keys", "-t", target, "Enter"]);
|
|
259
|
-
console.log(
|
|
260
|
-
`[orchestration] Task sent successfully to ${tmuxSessionName}`
|
|
261
|
-
);
|
|
262
|
-
} catch (sendError) {
|
|
263
|
-
console.error(
|
|
264
|
-
`[orchestration] Failed to send task to ${tmuxSessionName}:`,
|
|
265
|
-
sendError
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Update worker status to running
|
|
270
|
-
queries.updateWorkerStatus(db).run("running", sessionId);
|
|
271
|
-
} catch (error) {
|
|
272
|
-
console.error("Failed to start worker session:", error);
|
|
273
|
-
queries.updateWorkerStatus(db).run("failed", sessionId);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return queries.getSession(db).get(sessionId) as Session;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Get all workers for a conductor session
|
|
281
|
-
*/
|
|
282
|
-
export async function getWorkers(
|
|
283
|
-
conductorSessionId: string
|
|
284
|
-
): Promise<WorkerInfo[]> {
|
|
285
|
-
const workers = queries
|
|
286
|
-
.getWorkersByConductor(db)
|
|
287
|
-
.all(conductorSessionId) as Session[];
|
|
288
|
-
|
|
289
|
-
// Get live status for each worker
|
|
290
|
-
const workerInfos: WorkerInfo[] = [];
|
|
291
|
-
|
|
292
|
-
for (const worker of workers) {
|
|
293
|
-
const provider = getProvider(worker.agent_type || "claude");
|
|
294
|
-
const tmuxSessionName = worker.tmux_name || `${provider.id}-${worker.id}`;
|
|
295
|
-
|
|
296
|
-
// Get live status from tmux
|
|
297
|
-
let liveStatus: string;
|
|
298
|
-
try {
|
|
299
|
-
liveStatus = await statusDetector.getStatus(tmuxSessionName);
|
|
300
|
-
} catch {
|
|
301
|
-
liveStatus = "dead";
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Combine DB status with live status
|
|
305
|
-
let status: WorkerInfo["status"];
|
|
306
|
-
if (
|
|
307
|
-
worker.worker_status === "completed" ||
|
|
308
|
-
worker.worker_status === "failed"
|
|
309
|
-
) {
|
|
310
|
-
status = worker.worker_status;
|
|
311
|
-
} else if (liveStatus === "dead") {
|
|
312
|
-
status = "dead";
|
|
313
|
-
} else {
|
|
314
|
-
status = liveStatus as WorkerInfo["status"];
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
workerInfos.push({
|
|
318
|
-
id: worker.id,
|
|
319
|
-
name: worker.name,
|
|
320
|
-
task: worker.worker_task || "",
|
|
321
|
-
status,
|
|
322
|
-
worktreePath: worker.worktree_path,
|
|
323
|
-
branchName: worker.branch_name,
|
|
324
|
-
createdAt: worker.created_at,
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
return workerInfos;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Get recent output from a worker's terminal
|
|
333
|
-
*/
|
|
334
|
-
export async function getWorkerOutput(
|
|
335
|
-
workerId: string,
|
|
336
|
-
lines: number = 50
|
|
337
|
-
): Promise<string> {
|
|
338
|
-
const session = queries.getSession(db).get(workerId) as Session | undefined;
|
|
339
|
-
if (!session) {
|
|
340
|
-
throw new Error(`Worker ${workerId} not found`);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const provider = getProvider(session.agent_type || "claude");
|
|
344
|
-
const tmuxSessionName = session.tmux_name || `${provider.id}-${workerId}`;
|
|
345
|
-
|
|
346
|
-
try {
|
|
347
|
-
const stdout = await tmux([
|
|
348
|
-
"capture-pane",
|
|
349
|
-
"-t",
|
|
350
|
-
`=${tmuxSessionName}:`,
|
|
351
|
-
"-p",
|
|
352
|
-
"-S",
|
|
353
|
-
`-${Math.max(0, Math.floor(Number(lines)) || 0)}`,
|
|
354
|
-
]);
|
|
355
|
-
return stdout.trim();
|
|
356
|
-
} catch {
|
|
357
|
-
return "";
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Send a message/command to a worker
|
|
363
|
-
*/
|
|
364
|
-
export async function sendToWorker(
|
|
365
|
-
workerId: string,
|
|
366
|
-
message: string
|
|
367
|
-
): Promise<boolean> {
|
|
368
|
-
const session = queries.getSession(db).get(workerId) as Session | undefined;
|
|
369
|
-
if (!session) {
|
|
370
|
-
throw new Error(`Worker ${workerId} not found`);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const provider = getProvider(session.agent_type || "claude");
|
|
374
|
-
const tmuxSessionName = session.tmux_name || `${provider.id}-${workerId}`;
|
|
375
|
-
|
|
376
|
-
try {
|
|
377
|
-
const target = `=${tmuxSessionName}:`;
|
|
378
|
-
await tmux(["send-keys", "-t", target, "-l", "--", message]);
|
|
379
|
-
await tmux(["send-keys", "-t", target, "Enter"]);
|
|
380
|
-
return true;
|
|
381
|
-
} catch {
|
|
382
|
-
return false;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Mark a worker as completed
|
|
388
|
-
*/
|
|
389
|
-
export function completeWorker(workerId: string): void {
|
|
390
|
-
queries.updateWorkerStatus(db).run("completed", workerId);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Mark a worker as failed
|
|
395
|
-
*/
|
|
396
|
-
export function failWorker(workerId: string): void {
|
|
397
|
-
queries.updateWorkerStatus(db).run("failed", workerId);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Kill a worker session and optionally clean up its worktree
|
|
402
|
-
*/
|
|
403
|
-
export async function killWorker(
|
|
404
|
-
workerId: string,
|
|
405
|
-
cleanupWorktree: boolean = false
|
|
406
|
-
): Promise<void> {
|
|
407
|
-
const session = queries.getSession(db).get(workerId) as Session | undefined;
|
|
408
|
-
if (!session) {
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const provider = getProvider(session.agent_type || "claude");
|
|
413
|
-
const tmuxSessionName = session.tmux_name || `${provider.id}-${workerId}`;
|
|
414
|
-
|
|
415
|
-
// Kill tmux session
|
|
416
|
-
await endTmuxSession(session.host_id, tmuxSessionName);
|
|
417
|
-
|
|
418
|
-
// Clean up worktree if requested
|
|
419
|
-
// Note: This requires knowing the original project path, which we derive from git
|
|
420
|
-
if (cleanupWorktree && session.worktree_path) {
|
|
421
|
-
try {
|
|
422
|
-
// Get the main worktree (original project) from git
|
|
423
|
-
const projectPath = await mainCheckoutOf(session.worktree_path);
|
|
424
|
-
if (projectPath && projectPath !== session.worktree_path) {
|
|
425
|
-
await deleteWorktree(session.worktree_path, projectPath, true);
|
|
426
|
-
}
|
|
427
|
-
} catch (error) {
|
|
428
|
-
console.error("Failed to delete worktree:", error);
|
|
429
|
-
// Fallback: just remove the directory
|
|
430
|
-
try {
|
|
431
|
-
await rm(session.worktree_path, { recursive: true, force: true });
|
|
432
|
-
} catch {
|
|
433
|
-
// Ignore cleanup errors
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
queries.updateWorkerStatus(db).run("failed", workerId);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Get a summary of all workers' statuses
|
|
443
|
-
*/
|
|
444
|
-
export async function getWorkersSummary(conductorSessionId: string): Promise<{
|
|
445
|
-
total: number;
|
|
446
|
-
pending: number;
|
|
447
|
-
running: number;
|
|
448
|
-
waiting: number;
|
|
449
|
-
completed: number;
|
|
450
|
-
failed: number;
|
|
451
|
-
}> {
|
|
452
|
-
const workers = await getWorkers(conductorSessionId);
|
|
453
|
-
|
|
454
|
-
return {
|
|
455
|
-
total: workers.length,
|
|
456
|
-
pending: workers.filter((w) => w.status === "pending").length,
|
|
457
|
-
running: workers.filter((w) => w.status === "running").length,
|
|
458
|
-
waiting: workers.filter((w) => w.status === "waiting").length,
|
|
459
|
-
completed: workers.filter((w) => w.status === "completed").length,
|
|
460
|
-
failed: workers.filter((w) => w.status === "failed" || w.status === "dead")
|
|
461
|
-
.length,
|
|
462
|
-
};
|
|
463
|
-
}
|