@saadnvd1/agent-os 0.3.824 → 0.3.826

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 (68) hide show
  1. package/app/api/fork/cancel/route.ts +19 -0
  2. package/app/api/fork/children/route.ts +48 -0
  3. package/app/api/fork/result/route.ts +29 -0
  4. package/app/api/fork/return/route.ts +20 -0
  5. package/app/api/fork/route.ts +32 -0
  6. package/app/api/fork/wait/route.ts +27 -0
  7. package/app/api/load/heavy/lease/route.ts +26 -0
  8. package/app/api/sessions/[id]/archive/route.ts +17 -4
  9. package/app/api/sessions/[id]/route.ts +2 -3
  10. package/app/page.tsx +0 -5
  11. package/components/Chat/ChatPanel.tsx +33 -206
  12. package/components/Chat/Forks.tsx +278 -0
  13. package/components/Chat/ReplyingTo.tsx +24 -0
  14. package/components/Chat/TimelineRows.tsx +194 -0
  15. package/components/Pane/DesktopTabBar.tsx +0 -6
  16. package/components/Pane/MobileTabBar.tsx +0 -6
  17. package/components/Pane/MobileViewPicker.test.ts +1 -8
  18. package/components/Pane/MobileViewPicker.tsx +0 -11
  19. package/components/Pane/PaneViewToggle.test.ts +0 -2
  20. package/components/Pane/PaneViewToggle.tsx +0 -17
  21. package/components/Pane/index.tsx +0 -50
  22. package/components/SessionList/useDoneAction.ts +18 -3
  23. package/contexts/PaneContext.tsx +1 -1
  24. package/data/done/index.ts +26 -2
  25. package/data/fork/index.ts +83 -0
  26. package/lib/chat/commands.ts +15 -0
  27. package/lib/chat/runner.ts +8 -2
  28. package/lib/chat/worker/extras.ts +7 -1
  29. package/lib/chat/worker/host.ts +2 -0
  30. package/lib/config/client-config-types.ts +7 -0
  31. package/lib/config/client-config.ts +2 -0
  32. package/lib/db/migrations/20261102T0100_forks.ts +87 -0
  33. package/lib/done/archive.ts +21 -1
  34. package/lib/experiments/registry.ts +8 -0
  35. package/lib/fork/api.ts +10 -0
  36. package/lib/fork/brief.ts +63 -0
  37. package/lib/fork/conversation.ts +75 -0
  38. package/lib/fork/deliver.ts +91 -0
  39. package/lib/fork/extras.ts +53 -0
  40. package/lib/fork/guard.ts +323 -0
  41. package/lib/fork/heavy-lease.ts +104 -0
  42. package/lib/fork/held.ts +26 -0
  43. package/lib/fork/ledger.ts +324 -0
  44. package/lib/fork/options.ts +46 -0
  45. package/lib/fork/point.ts +41 -0
  46. package/lib/fork/scratch.ts +67 -0
  47. package/lib/fork/settle.ts +215 -0
  48. package/lib/fork/snapshot.ts +69 -0
  49. package/lib/fork/start.ts +273 -0
  50. package/lib/fork/tick.ts +54 -0
  51. package/lib/fork/types.ts +79 -0
  52. package/lib/fork/view.ts +116 -0
  53. package/lib/fork/waits.ts +83 -0
  54. package/lib/fork/worktree.ts +115 -0
  55. package/lib/orchestrator/shell-guard.ts +6 -3
  56. package/lib/orchestrator/write-guard.ts +1 -1
  57. package/lib/security/demo.ts +1 -2
  58. package/lib/sessions/end.ts +5 -0
  59. package/package.json +1 -1
  60. package/server.ts +2 -0
  61. package/app/api/orchestrate/spawn/route.ts +0 -54
  62. package/app/api/orchestrate/workers/[id]/route.ts +0 -89
  63. package/app/api/orchestrate/workers/route.ts +0 -31
  64. package/app/api/sessions/[id]/mcp-config/route.ts +0 -34
  65. package/components/ConductorPanel.tsx +0 -258
  66. package/lib/mcp-config.ts +0 -80
  67. package/lib/orchestration.ts +0 -463
  68. 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
- }
@@ -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
- }