fix: orchestrator button + Pi SDK session error

- Move leftSidebarMode from URL state to local useState in unified-shell,
    avoiding force-dynamic router round-trip that made the button appear broken                                           - Replace fileURLToPath(new URL(..., import.meta.url)) with process.cwd()
    in bb-pi-bootstrap.ts — import.meta.url is a webpack:// URL in Next.js,
    causing cross-realm TypeError when passed to Node.js fileURLToPath()
This commit is contained in:
zenchantlive 2026-03-24 15:39:19 -07:00
parent 643fa299dd
commit d335e5bf71
98 changed files with 17851 additions and 944 deletions

View file

@ -0,0 +1,38 @@
import { NextResponse } from 'next/server';
import { workerSessionManager, type WorkerSession } from '../../../../../lib/worker-session-manager';
export const dynamic = 'force-dynamic';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const projectRoot = searchParams.get('projectRoot');
if (!projectRoot) {
return NextResponse.json({ ok: false, error: 'projectRoot required' });
}
// Get completed/failed workers as history
const workers = workerSessionManager.listWorkers(projectRoot);
const history = workers
.filter((w: WorkerSession) => w.status === 'completed' || w.status === 'failed')
.sort((a: WorkerSession, b: WorkerSession) =>
new Date(b.completedAt || 0).getTime() - new Date(a.completedAt || 0).getTime()
)
.slice(0, 50)
.map((w: WorkerSession) => ({
id: w.agentInstanceId || w.id,
agentTypeId: w.agentTypeId || 'unknown',
displayName: w.displayName || `Worker ${w.id}`,
status: w.status,
currentBeadId: w.taskId,
startedAt: w.createdAt,
completedAt: w.completedAt,
result: w.result,
error: w.error,
}));
return NextResponse.json({
ok: true,
instances: history,
});
}

View file

@ -0,0 +1,42 @@
import { NextResponse } from 'next/server';
import { workerSessionManager, type WorkerSession } from '../../../../lib/worker-session-manager';
export const dynamic = 'force-dynamic';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const projectRoot = searchParams.get('projectRoot');
if (!projectRoot) {
return NextResponse.json({ ok: false, error: 'projectRoot required' });
}
const workers = workerSessionManager.listWorkers(projectRoot);
const instances = workers.map((w: WorkerSession) => ({
id: w.agentInstanceId || w.id,
agentTypeId: w.agentTypeId || 'unknown',
displayName: w.displayName || `Worker ${w.id}`,
status: w.status,
currentBeadId: w.taskId,
startedAt: w.createdAt,
completedAt: w.completedAt,
result: w.result,
error: w.error,
}));
const byType: Record<string, number> = {};
for (const w of workers) {
const typeId = w.agentTypeId || 'unknown';
byType[typeId] = (byType[typeId] || 0) + 1;
}
return NextResponse.json({
ok: true,
status: {
totalActive: workers.filter((w: WorkerSession) => w.status === 'working' || w.status === 'spawning').length,
byType,
instances,
},
});
}

View file

@ -0,0 +1,25 @@
import { NextResponse } from 'next/server';
import { bootstrapManagedPi } from '../../../../lib/bb-pi-bootstrap';
export const dynamic = 'force-dynamic';
export async function POST(): Promise<Response> {
try {
const result = await bootstrapManagedPi();
return NextResponse.json({
ok: true,
managedRoot: result.managedRoot,
sdkPath: result.sdkPath,
agentDir: result.agentDir,
alreadyInstalled: result.alreadyInstalled,
created: result.created,
});
} catch (error) {
console.error('[Bootstrap API] Error:', error);
return NextResponse.json(
{ ok: false, error: error instanceof Error ? error.message : 'Bootstrap failed' },
{ status: 500 }
);
}
}

View file

@ -0,0 +1,16 @@
import { NextResponse } from 'next/server';
import { bbDaemon } from '../../../../lib/bb-daemon';
export const dynamic = 'force-dynamic';
export async function GET(request: Request): Promise<Response> {
const { searchParams } = new URL(request.url);
const projectRoot = searchParams.get('projectRoot');
if (!projectRoot) {
return NextResponse.json({ ok: false, error: 'projectRoot is required' }, { status: 400 });
}
await bbDaemon.ensureRunning();
return NextResponse.json({ ok: true, lifecycle: bbDaemon.getLifecycle(), data: bbDaemon.listEvents(projectRoot) });
}

View file

@ -0,0 +1,55 @@
import { NextResponse } from 'next/server';
import { bbDaemon } from '../../../../lib/bb-daemon';
import type { LaunchSurface } from '../../../../lib/embedded-runtime';
import type { readIssuesFromDisk as ReadIssuesFromDisk } from '../../../../lib/read-issues';
export const dynamic = 'force-dynamic';
interface LaunchDeps {
readIssues?: typeof ReadIssuesFromDisk;
}
function isLaunchSurface(value: string): value is LaunchSurface {
return ['social', 'graph', 'swarm', 'sessions', 'activity', 'task'].includes(value);
}
export async function handleRuntimeLaunchPost(request: Request, deps: LaunchDeps = {}): Promise<Response> {
try {
const body = await request.json();
const projectRoot = typeof body?.projectRoot === 'string' ? body.projectRoot.trim() : '';
const taskId = typeof body?.taskId === 'string' ? body.taskId.trim() : '';
const origin = typeof body?.origin === 'string' && isLaunchSurface(body.origin) ? body.origin : null;
const swarmId = typeof body?.swarmId === 'string' ? body.swarmId : null;
if (!projectRoot || !taskId || !origin) {
return NextResponse.json({ ok: false, error: 'projectRoot, taskId, and origin are required' }, { status: 400 });
}
const read = deps.readIssues ?? (await import('../../../../lib/read-issues')).readIssuesFromDisk;
const issues = await read({ projectRoot, preferBd: true });
const issue = issues.find((entry) => entry.id === taskId);
if (!issue) {
return NextResponse.json({ ok: false, error: 'task not found' }, { status: 404 });
}
const lifecycle = await bbDaemon.ensureRunning();
const result = await bbDaemon.launchFromIssue({
projectRoot,
issue,
origin,
swarmId,
});
return NextResponse.json({ ok: true, lifecycle, data: result });
} catch (error) {
return NextResponse.json(
{ ok: false, error: error instanceof Error ? error.message : 'Invalid request' },
{ status: 400 },
);
}
}
export async function POST(request: Request): Promise<Response> {
return handleRuntimeLaunchPost(request);
}

View file

@ -0,0 +1,24 @@
import { NextResponse } from 'next/server';
import { bbDaemon } from '../../../../lib/bb-daemon';
export const dynamic = 'force-dynamic';
export async function POST(request: Request): Promise<Response> {
try {
const body = await request.json();
const projectRoot = typeof body?.projectRoot === 'string' ? body.projectRoot.trim() : '';
if (!projectRoot) {
return NextResponse.json({ ok: false, error: 'projectRoot is required' }, { status: 400 });
}
const lifecycle = await bbDaemon.ensureRunning();
const orchestrator = await bbDaemon.ensureOrchestrator(projectRoot);
return NextResponse.json({ ok: true, lifecycle, data: orchestrator });
} catch (error) {
return NextResponse.json(
{ ok: false, error: error instanceof Error ? error.message : 'Invalid request' },
{ status: 400 },
);
}
}

View file

@ -0,0 +1,29 @@
import { NextResponse } from 'next/server';
import { bbDaemon } from '../../../../lib/bb-daemon';
export const dynamic = 'force-dynamic';
export async function POST(request: Request): Promise<Response> {
try {
const body = await request.json();
const projectRoot = typeof body?.projectRoot === 'string' ? body.projectRoot.trim() : '';
const text = typeof body?.text === 'string' ? body.text.trim() : '';
if (!projectRoot || !text) {
return NextResponse.json({ ok: false, error: 'projectRoot and text are required' }, { status: 400 });
}
await bbDaemon.ensureRunning();
if (typeof (bbDaemon as any).prompt === 'function') {
void (bbDaemon as any).prompt(projectRoot, text);
}
return NextResponse.json({ ok: true });
} catch (error) {
return NextResponse.json(
{ ok: false, error: error instanceof Error ? error.message : 'Invalid request' },
{ status: 400 },
);
}
}

View file

@ -0,0 +1,49 @@
// src/app/api/runtime/spawn/route.ts
import { NextResponse } from 'next/server';
import { workerSessionManager } from '../../../../lib/worker-session-manager';
export async function POST(request: Request) {
try {
const body = await request.json();
const { projectRoot, beadId, agentTypeId } = body;
// Validate required fields
if (!projectRoot) {
return NextResponse.json(
{ ok: false, error: 'projectRoot is required' },
{ status: 400 }
);
}
if (!beadId) {
return NextResponse.json(
{ ok: false, error: 'beadId is required' },
{ status: 400 }
);
}
if (!agentTypeId) {
return NextResponse.json(
{ ok: false, error: 'agentTypeId is required' },
{ status: 400 }
);
}
// Spawn worker via session manager
const worker = await workerSessionManager.spawnWorker({
projectRoot,
taskId: beadId,
taskContext: `Work on ${beadId}`,
agentType: agentTypeId,
beadId,
});
return NextResponse.json({
ok: true,
workerId: worker.id,
displayName: worker.displayName,
agentTypeId: worker.agentTypeId,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return NextResponse.json({ ok: false, error: message });
}
}

View file

@ -0,0 +1,8 @@
import { NextResponse } from 'next/server';
import { bbDaemon } from '../../../../lib/bb-daemon';
export const dynamic = 'force-dynamic';
export async function GET(): Promise<Response> {
return NextResponse.json(bbDaemon.getStatus());
}

View file

@ -0,0 +1,80 @@
import { bbDaemon } from '../../../../lib/bb-daemon';
export const dynamic = 'force-dynamic';
const encoder = new TextEncoder();
const HEARTBEAT_MS = 15_000;
const POLL_MS = 250;
function toRuntimeSseFrame(event: unknown): string {
return `event: runtime\ndata: ${JSON.stringify(event)}\n\n`;
}
export async function GET(request: Request): Promise<Response> {
const { searchParams } = new URL(request.url);
const projectRoot = searchParams.get('projectRoot');
if (!projectRoot) {
return Response.json({ ok: false, error: 'projectRoot is required' }, { status: 400 });
}
await bbDaemon.start();
let cleanup = () => {};
const stream = new ReadableStream<Uint8Array>({
start(controller) {
let closed = false;
const write = (payload: string) => {
if (!closed) controller.enqueue(encoder.encode(payload));
};
write(': connected\n\n');
const seenIds = new Set<string>();
const seed = bbDaemon.listEvents(projectRoot);
for (const event of seed) {
seenIds.add(event.id);
write(toRuntimeSseFrame(event));
}
const poll = setInterval(() => {
const current = bbDaemon.listEvents(projectRoot);
const unseen = current.filter((event) => !seenIds.has(event.id));
for (const event of unseen.reverse()) {
seenIds.add(event.id);
write(toRuntimeSseFrame(event));
}
}, POLL_MS);
const heartbeat = setInterval(() => {
write(': heartbeat\n\n');
}, HEARTBEAT_MS);
const close = () => {
if (closed) return;
closed = true;
clearInterval(heartbeat);
clearInterval(poll);
request.signal.removeEventListener('abort', close);
try {
controller.close();
} catch {}
};
cleanup = close;
request.signal.addEventListener('abort', close);
},
cancel() {
cleanup();
return Promise.resolve();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
},
});
}

View file

@ -0,0 +1,37 @@
// src/app/api/runtime/worker-status/route.ts
import { NextResponse } from 'next/server';
import { workerSessionManager } from '../../../../lib/worker-session-manager';
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const beadId = searchParams.get('beadId');
if (!beadId) {
return NextResponse.json({ ok: false, error: 'beadId required' }, { status: 400 });
}
// Find worker for this bead
const workers = workerSessionManager.getAllWorkers();
const worker = workers.find(w => w.beadId === beadId);
if (!worker) {
return NextResponse.json({
ok: true,
workerStatus: 'idle',
agentTypeId: null,
});
}
return NextResponse.json({
ok: true,
workerStatus: worker.status,
workerDisplayName: worker.displayName,
workerError: worker.error,
agentTypeId: worker.agentTypeId,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return NextResponse.json({ ok: false, error: message });
}
}