@saadnvd1/agent-os 0.3.821 → 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.
Files changed (70) hide show
  1. package/app/api/fork/cancel/route.ts +19 -0
  2. package/app/api/fork/children/route.ts +52 -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/content/whats-new.json +16 -0
  24. package/contexts/PaneContext.tsx +1 -1
  25. package/data/done/index.ts +26 -2
  26. package/data/fork/index.ts +83 -0
  27. package/lib/chat/commands.ts +15 -0
  28. package/lib/chat/drivers/copilot-ask.ts +38 -5
  29. package/lib/chat/runner.ts +8 -2
  30. package/lib/chat/worker/extras.ts +7 -1
  31. package/lib/chat/worker/host.ts +2 -0
  32. package/lib/config/client-config-types.ts +7 -0
  33. package/lib/config/client-config.ts +2 -0
  34. package/lib/db/migrations/20261102T0100_forks.ts +87 -0
  35. package/lib/done/archive.ts +21 -1
  36. package/lib/experiments/registry.ts +8 -0
  37. package/lib/fork/api.ts +10 -0
  38. package/lib/fork/brief.ts +63 -0
  39. package/lib/fork/conversation.ts +75 -0
  40. package/lib/fork/deliver.ts +91 -0
  41. package/lib/fork/extras.ts +53 -0
  42. package/lib/fork/guard.ts +323 -0
  43. package/lib/fork/heavy-lease.ts +104 -0
  44. package/lib/fork/held.ts +26 -0
  45. package/lib/fork/ledger.ts +324 -0
  46. package/lib/fork/options.ts +46 -0
  47. package/lib/fork/point.ts +41 -0
  48. package/lib/fork/scratch.ts +67 -0
  49. package/lib/fork/settle.ts +215 -0
  50. package/lib/fork/snapshot.ts +69 -0
  51. package/lib/fork/start.ts +273 -0
  52. package/lib/fork/tick.ts +54 -0
  53. package/lib/fork/types.ts +79 -0
  54. package/lib/fork/view.ts +116 -0
  55. package/lib/fork/waits.ts +83 -0
  56. package/lib/fork/worktree.ts +115 -0
  57. package/lib/orchestrator/shell-guard.ts +6 -3
  58. package/lib/orchestrator/write-guard.ts +1 -1
  59. package/lib/security/demo.ts +1 -2
  60. package/lib/sessions/end.ts +5 -0
  61. package/package.json +1 -1
  62. package/server.ts +2 -0
  63. package/app/api/orchestrate/spawn/route.ts +0 -54
  64. package/app/api/orchestrate/workers/[id]/route.ts +0 -89
  65. package/app/api/orchestrate/workers/route.ts +0 -31
  66. package/app/api/sessions/[id]/mcp-config/route.ts +0 -34
  67. package/components/ConductorPanel.tsx +0 -258
  68. package/lib/mcp-config.ts +0 -80
  69. package/lib/orchestration.ts +0 -463
  70. package/mcp/orchestration-server.ts +0 -441
@@ -0,0 +1,116 @@
1
+ /** What clients and `aos children` show, and the numbers behind them. */
2
+
3
+ import { childrenOf, getGroup } from "./ledger";
4
+ import type { ChildRow, ChildView, GroupView } from "./types";
5
+
6
+ export const childView = (c: ChildRow): ChildView => ({
7
+ id: c.child_session_id,
8
+ n: 0,
9
+ group: c.group_id,
10
+ label: c.label,
11
+ instruction: c.instruction,
12
+ state: c.state,
13
+ hold: c.hold,
14
+ outcome: c.outcome,
15
+ summary: c.summary,
16
+ resultKind: c.result_kind,
17
+ worktree: !!c.worktree,
18
+ fellBack: !!c.fell_back,
19
+ startedAt: c.started_at,
20
+ settledAt: c.settled_at,
21
+ delivered: c.delivered_at != null,
22
+ asks: c.state === "needs_input" ? c.hold : null,
23
+ });
24
+
25
+ export function childrenView(parentId: string): {
26
+ children: ChildView[];
27
+ groups: GroupView[];
28
+ } {
29
+ const rows = childrenOf(parentId);
30
+ const seen = new Set<string>();
31
+ const groups: GroupView[] = [];
32
+ for (const c of rows) {
33
+ if (seen.has(c.group_id)) continue;
34
+ seen.add(c.group_id);
35
+ const g = getGroup(c.group_id);
36
+ if (g)
37
+ groups.push({
38
+ id: g.id,
39
+ delivery: g.delivery,
40
+ context: g.context,
41
+ startedBy: g.started_by,
42
+ createdAt: g.created_at,
43
+ stats: null,
44
+ });
45
+ }
46
+ return {
47
+ children: rows.map((c, i) => ({ ...childView(c), n: i + 1 })),
48
+ groups,
49
+ };
50
+ }
51
+
52
+ const median = (xs: number[]): number | null => {
53
+ if (!xs.length) return null;
54
+ const s = [...xs].sort((a, b) => a - b);
55
+ return s[Math.floor(s.length / 2)];
56
+ };
57
+
58
+ export interface ForkStats {
59
+ children: number;
60
+ settled: number;
61
+ ok: number;
62
+ failed: number;
63
+ cancelled: number;
64
+ implicit: number;
65
+ fellBack: number;
66
+ medianSeconds: number | null;
67
+ tokensIn: number;
68
+ tokensOut: number;
69
+ cacheRead: number;
70
+ heavyRuns: number;
71
+ delivered: number;
72
+ opened: number;
73
+ mentioned: number;
74
+ }
75
+
76
+ export function statsOf(rows: ChildRow[]): ForkStats {
77
+ const done = rows.filter((c) => c.state === "settled");
78
+ const sum = (f: (c: ChildRow) => number | null) =>
79
+ rows.reduce((n, c) => n + (f(c) ?? 0), 0);
80
+ return {
81
+ children: rows.length,
82
+ settled: done.length,
83
+ ok: done.filter((c) => c.outcome === "ok").length,
84
+ failed: done.filter((c) => c.outcome === "failed").length,
85
+ cancelled: done.filter((c) => c.outcome === "cancelled").length,
86
+ implicit: done.filter(
87
+ (c) => c.result_kind === "implicit" && c.outcome === "ok"
88
+ ).length,
89
+ fellBack: rows.filter((c) => c.fell_back).length,
90
+ medianSeconds: median(
91
+ done
92
+ .filter((c) => c.started_at && c.settled_at)
93
+ .map((c) => Math.round((c.settled_at! - c.started_at!) / 1000))
94
+ ),
95
+ tokensIn: sum((c) => c.tokens_in),
96
+ tokensOut: sum((c) => c.tokens_out),
97
+ cacheRead: sum((c) => c.cache_read),
98
+ heavyRuns: sum((c) => c.heavy_runs),
99
+ delivered: rows.filter((c) => c.delivered_at != null).length,
100
+ opened: rows.filter((c) => c.used === "opened").length,
101
+ mentioned: rows.filter((c) => c.used === "mentioned").length,
102
+ };
103
+ }
104
+
105
+ export function statsText(s: ForkStats): string {
106
+ const pct = (n: number) =>
107
+ s.settled ? `${Math.round((n / s.settled) * 100)}%` : "-";
108
+ return [
109
+ `${s.children} children, ${s.settled} settled (${s.ok} ok, ${s.failed} failed, ${s.cancelled} cancelled)`,
110
+ `returned without aos return: ${s.implicit} (${pct(s.implicit)}); started from a brief, not a fork: ${s.fellBack}`,
111
+ `median time to settle: ${s.medianSeconds ?? "-"}s`,
112
+ `tokens: ${s.tokensIn} in, ${s.tokensOut} out, ${s.cacheRead} cache read`,
113
+ `heavy runs queued: ${s.heavyRuns}`,
114
+ `results delivered: ${s.delivered}; opened in full: ${s.opened}; mentioned later: ${s.mentioned}`,
115
+ ].join("\n");
116
+ }
@@ -0,0 +1,83 @@
1
+ /** `aos wait children`: the parent ends its turn and is woken with results. */
2
+
3
+ import { db } from "../db";
4
+ import { numberOf, openChildren, childrenOf, undelivered } from "./ledger";
5
+ import { deliverNow } from "./deliver";
6
+
7
+ interface WaitRow {
8
+ id: number;
9
+ parent_session_id: string;
10
+ mode: "all" | "any";
11
+ created_at: number;
12
+ deadline: number;
13
+ }
14
+
15
+ export const MAX_WAIT_MS = 24 * 60 * 60_000;
16
+ export const DEFAULT_WAIT_MS = 30 * 60_000;
17
+
18
+ export const openForkWait = (parentId: string): WaitRow | undefined =>
19
+ db
20
+ .prepare(
21
+ `SELECT * FROM fork_waits WHERE parent_session_id = ? AND closed_at IS NULL`
22
+ )
23
+ .get(parentId) as WaitRow | undefined;
24
+
25
+ export function registerForkWait(
26
+ parentId: string,
27
+ mode: "all" | "any",
28
+ timeoutMs = DEFAULT_WAIT_MS,
29
+ now = Date.now()
30
+ ): { id: number; deadline: number } | { already: string } {
31
+ const open = childrenOf(parentId).filter((c) => c.state !== "settled");
32
+ const waiting = undelivered(parentId);
33
+ if (!open.length && !waiting.length)
34
+ return { already: "you have no running forks and no unread results" };
35
+ const deadline = now + Math.min(Math.max(timeoutMs, 60_000), MAX_WAIT_MS);
36
+ // One wait per parent: a new one replaces it.
37
+ db.prepare(
38
+ `UPDATE fork_waits SET closed_at = ? WHERE parent_session_id = ? AND closed_at IS NULL`
39
+ ).run(now, parentId);
40
+ const { lastInsertRowid } = db
41
+ .prepare(
42
+ `INSERT INTO fork_waits (parent_session_id, mode, created_at, deadline) VALUES (?, ?, ?, ?)`
43
+ )
44
+ .run(parentId, mode, now, deadline);
45
+ return { id: Number(lastInsertRowid), deadline };
46
+ }
47
+
48
+ const close = (id: number, now: number): boolean =>
49
+ db
50
+ .prepare(
51
+ `UPDATE fork_waits SET closed_at = ? WHERE id = ? AND closed_at IS NULL`
52
+ )
53
+ .run(now, id).changes === 1;
54
+
55
+ export async function tickForkWaits(now = Date.now()): Promise<void> {
56
+ const waits = db
57
+ .prepare(`SELECT * FROM fork_waits WHERE closed_at IS NULL`)
58
+ .all() as WaitRow[];
59
+ for (const w of waits) {
60
+ const ready = undelivered(w.parent_session_id);
61
+ const open = openChildren(w.parent_session_id);
62
+ const holds =
63
+ w.mode === "all"
64
+ ? open.length === 0
65
+ : ready.length > 0 || open.length === 0;
66
+ const timedOut = now >= w.deadline;
67
+ if (!holds && !timedOut) continue;
68
+ if (!close(w.id, now)) continue;
69
+ if (!ready.length && !timedOut) continue;
70
+ if (!ready.length) {
71
+ const { sendChat } = await import("../chat/runner");
72
+ await sendChat(w.parent_session_id, {
73
+ id: `fork-w-${w.id}`,
74
+ text: `[AgentOS: your wait on forks timed out; ${open.map((c) => numberOf(w.parent_session_id, c.child_session_id)).join(", ")} still running. \`aos children\` shows them.]`,
75
+ queue: true,
76
+ from: "AgentOS",
77
+ origin: { kind: "event", label: "Forks" },
78
+ });
79
+ continue;
80
+ }
81
+ await deliverNow(w.parent_session_id, ready);
82
+ }
83
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * A throwaway checkout for a fork child that has to edit code to find its
3
+ * answer (`aos fork --worktree`): detached at the parent's HEAD, with the
4
+ * parent's uncommitted changes applied on top, so it sees what the parent
5
+ * sees. No branch is made and nothing from it is merged; it is removed when
6
+ * the child settles.
7
+ */
8
+
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import { git } from "../git-exec";
12
+ import { setupWorktree } from "../env-setup";
13
+ import { forkHome, worktreeOf } from "./scratch";
14
+
15
+ const MAX_UNTRACKED = 200;
16
+ const MAX_UNTRACKED_BYTES = 5_000_000;
17
+ // A change to these means the parent's installed packages may not match.
18
+ const DEPENDENCY_FILES =
19
+ /(^|\/)(package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?)$/;
20
+
21
+ export interface ForkWorktree {
22
+ // Where the child runs: the parent's folder, in the checkout.
23
+ cwd: string;
24
+ // The checkout's root.
25
+ root: string;
26
+ // The parent's packages are linked in instead of installed.
27
+ linked: boolean;
28
+ // The commit it started from, for the child's return.
29
+ head: string;
30
+ }
31
+
32
+ export async function createForkWorktree(opts: {
33
+ childId: string;
34
+ parentCwd: string;
35
+ }): Promise<ForkWorktree> {
36
+ const cwd = opts.parentCwd;
37
+ const top = (await git(cwd, ["rev-parse", "--show-toplevel"])).trim();
38
+ const head = (await git(top, ["rev-parse", "HEAD"])).trim();
39
+ const wt = worktreeOf(opts.childId);
40
+ await fs.promises.mkdir(forkHome(opts.childId), { recursive: true });
41
+ try {
42
+ await git(top, ["worktree", "add", "--detach", "--", wt, head], {
43
+ timeout: 5 * 60_000,
44
+ });
45
+ const diff = await git(top, ["diff", "HEAD", "--binary"], {
46
+ maxBuffer: 64 * 1024 * 1024,
47
+ });
48
+ if (diff.trim())
49
+ await git(wt, ["apply", "--whitespace=nowarn", "--binary", "-"], {
50
+ input: diff,
51
+ });
52
+ await copyUntracked(top, wt);
53
+ const changed = (await git(top, ["diff", "HEAD", "--name-only"]))
54
+ .split("\n")
55
+ .filter(Boolean);
56
+ const hasPackages = await exists(path.join(top, "node_modules"));
57
+ const linked =
58
+ hasPackages && !changed.some((f) => DEPENDENCY_FILES.test(f));
59
+ if (linked)
60
+ await fs.promises.symlink(
61
+ path.join(top, "node_modules"),
62
+ path.join(wt, "node_modules")
63
+ );
64
+ const setup = await setupWorktree({
65
+ worktreePath: wt,
66
+ sourcePath: top,
67
+ skipInstall: linked,
68
+ });
69
+ if (!setup.success)
70
+ throw new Error(
71
+ setup.steps.find((s) => !s.success)?.error ?? "worktree setup failed"
72
+ );
73
+ return {
74
+ cwd: path.join(wt, path.relative(top, cwd)),
75
+ root: wt,
76
+ linked,
77
+ head,
78
+ };
79
+ } catch (error) {
80
+ const { removeFork } = await import("./scratch");
81
+ await removeFork(opts.childId);
82
+ throw error;
83
+ }
84
+ }
85
+
86
+ const exists = (p: string) =>
87
+ fs.promises.access(p).then(
88
+ () => true,
89
+ () => false
90
+ );
91
+
92
+ // Files the parent made and hasn't added yet, which a diff doesn't carry.
93
+ async function copyUntracked(top: string, wt: string): Promise<void> {
94
+ const out = await git(top, [
95
+ "ls-files",
96
+ "--others",
97
+ "--exclude-standard",
98
+ "-z",
99
+ ]);
100
+ const files = out.split("\0").filter(Boolean);
101
+ if (files.length > MAX_UNTRACKED)
102
+ throw new Error(
103
+ `the parent has ${files.length} new files; commit or stage them first`
104
+ );
105
+ for (const rel of files) {
106
+ const from = path.join(top, rel);
107
+ const stat = await fs.promises.lstat(from);
108
+ if (stat.isSymbolicLink() || !stat.isFile()) continue;
109
+ if (stat.size > MAX_UNTRACKED_BYTES)
110
+ throw new Error(`${rel} is too large to carry into a checkout`);
111
+ const to = path.join(wt, rel);
112
+ await fs.promises.mkdir(path.dirname(to), { recursive: true });
113
+ await fs.promises.copyFile(from, to);
114
+ }
115
+ }
@@ -37,7 +37,7 @@ export interface ShellContext {
37
37
  protectedPids: () => Promise<Set<number>>;
38
38
  }
39
39
 
40
- interface Invocation {
40
+ export interface Invocation {
41
41
  name: string;
42
42
  args: string[];
43
43
  cmd: SimpleCommand;
@@ -87,7 +87,10 @@ const SHELLS = /^(ba|z|da|k|fi|c|tc)?sh$/;
87
87
 
88
88
  // What a simple command runs once its wrappers are taken off: one
89
89
  // invocation, or several (`bash -c '…'`, `find -exec`).
90
- function invocations(cmd: SimpleCommand, depth = 0): Invocation[] | string {
90
+ export function invocations(
91
+ cmd: SimpleCommand,
92
+ depth = 0
93
+ ): Invocation[] | string {
91
94
  let words = cmd.words;
92
95
  for (;;) {
93
96
  while (
@@ -183,7 +186,7 @@ function findInvocations(
183
186
  }
184
187
 
185
188
  // Words that aren't options; `--` ends options. `values` take the next word.
186
- function positionals(args: string[], values: string[] = []): string[] {
189
+ export function positionals(args: string[], values: string[] = []): string[] {
187
190
  const out: string[] = [];
188
191
  for (let i = 0; i < args.length; i++) {
189
192
  const a = args[i];
@@ -130,7 +130,7 @@ const FOLDS_CASE =
130
130
  const key = (p: string) =>
131
131
  FOLDS_CASE ? p.normalize("NFC").toLowerCase() : p.normalize("NFC");
132
132
 
133
- const within = (p: string, root: string) => {
133
+ export const within = (p: string, root: string) => {
134
134
  const [a, b] = [key(p), key(root)];
135
135
  return a === b || a.startsWith(b === "/" ? b : b + "/");
136
136
  };
@@ -120,10 +120,9 @@ const READS = [
120
120
  "schedules",
121
121
  "schedules/[id]",
122
122
  "orchestrators",
123
- "orchestrate/workers",
124
- "orchestrate/workers/[id]",
125
123
  "bus/messages",
126
124
  "bus/peers",
125
+ "fork/children",
127
126
  "usage",
128
127
  "vendor/mermaid",
129
128
  "artifacts/[id]",
@@ -5,6 +5,8 @@ import { endTmuxSession } from "../tmux/end";
5
5
  import { releaseOnEnd, shutTaskSimulators } from "../simulator";
6
6
  import { forgetPlanWait } from "../models/plan-step";
7
7
  import { kickWaits } from "../bus/waits/kick";
8
+ import { openChildren } from "../fork/ledger";
9
+ import { cancelChildren } from "../fork/settle";
8
10
 
9
11
  type Agent = Pick<Session, "id" | "host_id" | "tmux_name"> &
10
12
  Partial<Pick<Session, "worktree_path" | "branch_name">>;
@@ -14,6 +16,9 @@ type Agent = Pick<Session, "id" | "host_id" | "tmux_name"> &
14
16
  // finish share.
15
17
  export async function endSessionAgent(s: Agent): Promise<void> {
16
18
  forgetPlanWait(s.id);
19
+ // Its forks have no one to report to.
20
+ if (openChildren(s.id).length)
21
+ void cancelChildren(s.id).catch(() => undefined);
17
22
  stopChat(s.id);
18
23
  // The task's simulator goes with it; no row, no work.
19
24
  releaseOnEnd(s.id, { remove: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.821",
3
+ "version": "0.3.825",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"
package/server.ts CHANGED
@@ -17,6 +17,7 @@ import { stackStartGate } from "./lib/orchestrator/brakes";
17
17
  import { buildId } from "./lib/build";
18
18
  import { startOrchestratorWatcher } from "./lib/orchestrator/watcher";
19
19
  import { startWaitWatcher } from "./lib/bus/waits/tick";
20
+ import { startForkWatcher } from "./lib/fork/tick";
20
21
  import { installErrorCounter } from "./lib/health/errors";
21
22
  import { recordBoot } from "./lib/health/boots";
22
23
  import { realDeps, schedulesEnabled, startScheduler } from "./lib/schedules";
@@ -498,6 +499,7 @@ app.prepare().then(async () => {
498
499
  if (process.env.AGENTOS_ORCHESTRATOR !== "off") startOrchestratorWatcher();
499
500
  // Sessions waiting on each other (aos send --await, aos wait).
500
501
  startWaitWatcher();
502
+ startForkWatcher();
501
503
  // Schedules tick here, once a minute, and nowhere else.
502
504
  if (schedulesEnabled(process.env)) startScheduler(realDeps);
503
505
  // Looking for a newer version, and running an update when one is asked
@@ -1,54 +0,0 @@
1
- import { NextResponse } from "next/server";
2
- import { resolveModelForAgent } from "@/lib/model-catalog";
3
- import { isValidAgentType, type AgentType } from "@/lib/providers";
4
- import { spawnWorker } from "@/lib/orchestration";
5
-
6
- export async function POST(request: Request) {
7
- try {
8
- const body = await request.json();
9
- const {
10
- conductorSessionId,
11
- task,
12
- workingDirectory,
13
- branchName,
14
- useWorktree = true,
15
- model,
16
- agentType: rawAgentType = "claude",
17
- } = body;
18
- const agentType: AgentType = isValidAgentType(rawAgentType)
19
- ? rawAgentType
20
- : "claude";
21
- const resolvedModel = resolveModelForAgent(
22
- agentType,
23
- typeof model === "string" ? model.trim() : model
24
- );
25
-
26
- if (!conductorSessionId || !task || !workingDirectory) {
27
- return NextResponse.json(
28
- {
29
- error:
30
- "Missing required fields: conductorSessionId, task, workingDirectory",
31
- },
32
- { status: 400 }
33
- );
34
- }
35
-
36
- const session = await spawnWorker({
37
- conductorSessionId,
38
- task,
39
- workingDirectory,
40
- branchName,
41
- useWorktree,
42
- model: resolvedModel,
43
- agentType,
44
- });
45
-
46
- return NextResponse.json({ session });
47
- } catch (error) {
48
- console.error("Failed to spawn worker:", error);
49
- return NextResponse.json(
50
- { error: "Failed to spawn worker" },
51
- { status: 500 }
52
- );
53
- }
54
- }
@@ -1,89 +0,0 @@
1
- import { NextResponse } from "next/server";
2
- import {
3
- getWorkerOutput,
4
- sendToWorker,
5
- completeWorker,
6
- failWorker,
7
- killWorker,
8
- } from "@/lib/orchestration";
9
-
10
- interface RouteParams {
11
- params: Promise<{ id: string }>;
12
- }
13
-
14
- // GET /api/orchestrate/workers/[id] - Get worker output
15
- export async function GET(request: Request, { params }: RouteParams) {
16
- try {
17
- const { id } = await params;
18
- const { searchParams } = new URL(request.url);
19
- const lines = parseInt(searchParams.get("lines") || "50", 10);
20
-
21
- const output = await getWorkerOutput(id, lines);
22
- return NextResponse.json({ output });
23
- } catch (error) {
24
- console.error("Failed to get worker output:", error);
25
- return NextResponse.json(
26
- { error: "Failed to get worker output" },
27
- { status: 500 }
28
- );
29
- }
30
- }
31
-
32
- // POST /api/orchestrate/workers/[id] - Send message or update status
33
- export async function POST(request: Request, { params }: RouteParams) {
34
- try {
35
- const { id } = await params;
36
- const body = await request.json();
37
- const { action, message } = body;
38
-
39
- switch (action) {
40
- case "send":
41
- if (!message) {
42
- return NextResponse.json(
43
- { error: "Missing message" },
44
- { status: 400 }
45
- );
46
- }
47
- const sent = await sendToWorker(id, message);
48
- return NextResponse.json({ success: sent });
49
-
50
- case "complete":
51
- completeWorker(id);
52
- return NextResponse.json({ success: true });
53
-
54
- case "fail":
55
- failWorker(id);
56
- return NextResponse.json({ success: true });
57
-
58
- default:
59
- return NextResponse.json(
60
- { error: "Invalid action. Use: send, complete, or fail" },
61
- { status: 400 }
62
- );
63
- }
64
- } catch (error) {
65
- console.error("Failed to perform worker action:", error);
66
- return NextResponse.json(
67
- { error: "Failed to perform worker action" },
68
- { status: 500 }
69
- );
70
- }
71
- }
72
-
73
- // DELETE /api/orchestrate/workers/[id] - Kill worker
74
- export async function DELETE(request: Request, { params }: RouteParams) {
75
- try {
76
- const { id } = await params;
77
- const { searchParams } = new URL(request.url);
78
- const cleanupWorktree = searchParams.get("cleanup") === "true";
79
-
80
- await killWorker(id, cleanupWorktree);
81
- return NextResponse.json({ success: true });
82
- } catch (error) {
83
- console.error("Failed to kill worker:", error);
84
- return NextResponse.json(
85
- { error: "Failed to kill worker" },
86
- { status: 500 }
87
- );
88
- }
89
- }
@@ -1,31 +0,0 @@
1
- import { NextResponse } from "next/server";
2
- import { getWorkers, getWorkersSummary } from "@/lib/orchestration";
3
-
4
- export async function GET(request: Request) {
5
- try {
6
- const { searchParams } = new URL(request.url);
7
- const conductorId = searchParams.get("conductorId");
8
- const summaryOnly = searchParams.get("summary") === "true";
9
-
10
- if (!conductorId) {
11
- return NextResponse.json(
12
- { error: "Missing conductorId parameter" },
13
- { status: 400 }
14
- );
15
- }
16
-
17
- if (summaryOnly) {
18
- const summary = await getWorkersSummary(conductorId);
19
- return NextResponse.json({ summary });
20
- }
21
-
22
- const workers = await getWorkers(conductorId);
23
- return NextResponse.json({ workers });
24
- } catch (error) {
25
- console.error("Failed to get workers:", error);
26
- return NextResponse.json(
27
- { error: "Failed to get workers" },
28
- { status: 500 }
29
- );
30
- }
31
- }
@@ -1,34 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { db, queries, type Session } from "@/lib/db";
3
- import { ensureMcpConfig } from "@/lib/mcp-config";
4
-
5
- // POST /api/sessions/[id]/mcp-config - Ensure MCP config exists for this session
6
- export async function POST(
7
- request: NextRequest,
8
- { params }: { params: Promise<{ id: string }> }
9
- ) {
10
- try {
11
- const { id } = await params;
12
- const session = queries.getSession(db).get(id) as Session | undefined;
13
-
14
- if (!session) {
15
- return NextResponse.json({ error: "Session not found" }, { status: 404 });
16
- }
17
-
18
- // Expand ~ to home directory
19
- const workingDirectory = session.working_directory.replace(
20
- /^~/,
21
- process.env.HOME || ""
22
- );
23
-
24
- ensureMcpConfig(workingDirectory, id);
25
-
26
- return NextResponse.json({ success: true });
27
- } catch (error) {
28
- console.error("Failed to write MCP config:", error);
29
- return NextResponse.json(
30
- { error: "Failed to write MCP config" },
31
- { status: 500 }
32
- );
33
- }
34
- }