import express from 'express'; import { createOpencodeClient } from '@opencode-ai/sdk/v2'; import { createWorktree } from '../git/index.js'; import { expandSnippets } from '../opencode/snippets.js'; import { expandCommandGoalObjective, parseScheduledCommandPrompt } from '../scheduled-tasks/runtime.js'; import { buildGoalIntroText, createSessionGoal } from '../session-goal/create.js'; import { OpenChamberControlError, asControlError } from '../openchamber-control/error.js'; const asNonEmptyString = (value) => { if (typeof value !== 'string') return null; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; }; const splitModel = (value) => { const model = asNonEmptyString(value); if (!model) return null; const slashIndex = model.indexOf('/'); if (slashIndex <= 0 || slashIndex === model.length - 1) return null; return { providerID: model.slice(0, slashIndex), modelID: model.slice(slashIndex + 1), }; }; const resolveRequestedModel = (payload) => { const model = splitModel(payload?.model); if (model) return model; const providerID = asNonEmptyString(payload?.providerID); const modelID = asNonEmptyString(payload?.modelID); return providerID && modelID ? { providerID, modelID } : null; }; const FALLBACK_PROVIDER_ID = 'opencode'; const FALLBACK_MODEL_ID = 'big-pickle'; const MIN_GOAL_TOKEN_BUDGET = 1_000; const MAX_GOAL_TOKEN_BUDGET = 100_000_000; const resolveGoalInput = (payload, prompt) => { const enabled = payload?.goal === true; if (payload?.goalTokenBudget !== undefined && !enabled) { return { ok: false, error: 'goalTokenBudget requires goal' }; } if (enabled && !prompt) { return { ok: false, error: 'prompt is required when goal is enabled' }; } if (payload?.goalTokenBudget === undefined) { return { ok: true, enabled, tokenBudget: null }; } const tokenBudget = payload.goalTokenBudget; if (!Number.isSafeInteger(tokenBudget) || tokenBudget < MIN_GOAL_TOKEN_BUDGET || tokenBudget > MAX_GOAL_TOKEN_BUDGET) { return { ok: false, error: `goalTokenBudget must be an integer from ${MIN_GOAL_TOKEN_BUDGET} to ${MAX_GOAL_TOKEN_BUDGET}` }; } return { ok: true, enabled, tokenBudget }; }; const isPrimaryAgentMode = (mode) => !mode || mode === 'primary' || mode === 'all'; const providerModels = (provider) => { if (Array.isArray(provider?.models)) return provider.models; if (provider?.models && typeof provider.models === 'object') return Object.values(provider.models); return []; }; const hasProviderModel = (providers, providerID, modelID) => { return providers.some((provider) => provider?.id === providerID && providerModels(provider).some((model) => model?.id === modelID)); }; const resolveVariant = (providers, providerID, modelID, variant) => { const normalized = asNonEmptyString(variant); if (!normalized) return undefined; const provider = providers.find((entry) => entry?.id === providerID); const model = providerModels(provider).find((entry) => entry?.id === modelID); return model?.variants && Object.prototype.hasOwnProperty.call(model.variants, normalized) ? normalized : undefined; }; const parseConfigModel = (value) => splitModel(value); const buildDirectoryHeaders = (directory) => ({ ...(directory ? { 'x-opencode-directory': directory } : {}), }); const fetchJson = async (url, authHeaders, fallback, directory) => { const response = await fetch(url.toString(), { headers: { ...authHeaders, ...buildDirectoryHeaders(directory), accept: 'application/json' }, }); if (!response.ok) return fallback; return response.json().catch(() => fallback); }; const fetchSelectionInputs = async ({ buildOpenCodeUrl, authHeaders, directory, readSettingsFromDiskMigrated }) => { const settings = await readSettingsFromDiskMigrated(); const providersUrl = new URL(buildOpenCodeUrl('/config/providers', '')); providersUrl.searchParams.set('directory', directory); const agentsUrl = new URL(buildOpenCodeUrl('/agent', '')); agentsUrl.searchParams.set('directory', directory); const configUrl = new URL(buildOpenCodeUrl('/config', '')); configUrl.searchParams.set('directory', directory); const [providersBody, agentsBody, configBody] = await Promise.all([ fetchJson(providersUrl, authHeaders, { providers: [] }, directory), fetchJson(agentsUrl, authHeaders, [], directory), fetchJson(configUrl, authHeaders, {}, directory), ]); return { settings, providers: Array.isArray(providersBody?.providers) ? providersBody.providers : [], agents: Array.isArray(agentsBody) ? agentsBody : [], opencodeDefaultAgent: asNonEmptyString(configBody?.default_agent) || asNonEmptyString(configBody?.defaultAgent), opencodeDefaultModel: asNonEmptyString(configBody?.model), }; }; const resolveDefaultSelection = ({ agents, providers, settings, opencodeDefaultAgent, opencodeDefaultModel }) => { const primaryAgents = agents.filter((agent) => isPrimaryAgentMode(agent?.mode) && agent?.hidden !== true); let resolvedAgent = null; const settingsDefaultAgent = asNonEmptyString(settings?.defaultAgent); if (settingsDefaultAgent) { resolvedAgent = agents.find((agent) => agent?.name === settingsDefaultAgent) || null; } if (!resolvedAgent && opencodeDefaultAgent) { const candidate = agents.find((agent) => agent?.name === opencodeDefaultAgent) || null; if (candidate && isPrimaryAgentMode(candidate.mode) && candidate.hidden !== true) { resolvedAgent = candidate; } } if (!resolvedAgent) { resolvedAgent = primaryAgents.find((agent) => agent?.name === 'build') || primaryAgents[0] || agents[0] || null; } let model = null; let variant; const settingsDefaultModel = parseConfigModel(settings?.defaultModel); if (settingsDefaultModel && hasProviderModel(providers, settingsDefaultModel.providerID, settingsDefaultModel.modelID)) { model = settingsDefaultModel; variant = resolveVariant(providers, model.providerID, model.modelID, settings?.defaultVariant); } if (!model && resolvedAgent?.model?.providerID && resolvedAgent?.model?.modelID && hasProviderModel(providers, resolvedAgent.model.providerID, resolvedAgent.model.modelID)) { model = { providerID: resolvedAgent.model.providerID, modelID: resolvedAgent.model.modelID }; variant = resolveVariant(providers, model.providerID, model.modelID, resolvedAgent.variant); } const opencodeModel = parseConfigModel(opencodeDefaultModel); if (!model && opencodeModel && hasProviderModel(providers, opencodeModel.providerID, opencodeModel.modelID)) { model = opencodeModel; } if (!model && hasProviderModel(providers, FALLBACK_PROVIDER_ID, FALLBACK_MODEL_ID)) { model = { providerID: FALLBACK_PROVIDER_ID, modelID: FALLBACK_MODEL_ID }; } if (!model) { const provider = providers[0]; const firstModel = providerModels(provider)[0]; if (provider?.id && firstModel?.id) { model = { providerID: provider.id, modelID: firstModel.id }; } } return { agent: resolvedAgent?.name, model, variant, }; }; const runPromptAsync = async ({ baseUrl, authHeaders, sessionID, directory, payload }) => { const promptUrl = new URL(`${baseUrl}/session/${encodeURIComponent(sessionID)}/prompt_async`); promptUrl.searchParams.set('directory', directory); const response = await fetch(promptUrl.toString(), { method: 'POST', headers: { ...authHeaders, ...buildDirectoryHeaders(directory), 'content-type': 'application/json', accept: 'application/json', }, body: JSON.stringify(payload), }); if (!response.ok) { const body = await response.text().catch(() => ''); throw new Error(`prompt_async failed (${response.status})${body ? `: ${body}` : ''}`); } }; const createSession = async ({ baseUrl, authHeaders, directory, title }) => { const sessionUrl = new URL(`${baseUrl}/session`); sessionUrl.searchParams.set('directory', directory); const response = await fetch(sessionUrl.toString(), { method: 'POST', headers: { ...authHeaders, ...buildDirectoryHeaders(directory), 'content-type': 'application/json', accept: 'application/json', }, body: JSON.stringify({ directory, ...(title ? { title } : {}) }), }); if (!response.ok) { const body = await response.text().catch(() => ''); throw new Error(`session create failed (${response.status})${body ? `: ${body}` : ''}`); } const body = await response.json().catch(() => null); const sessionID = body?.id || body?.data?.id; if (!sessionID) { throw new Error('failed to create session'); } return sessionID; }; const forkSession = async ({ client, sessionID, directory, messageID }) => { const response = await client.session.fork({ sessionID, directory, ...(messageID ? { messageID } : {}), }); const session = response?.data; if (!session?.id) { throw new Error('failed to fork session'); } return session; }; const latestCompletedAssistantMessageID = async ({ client, sessionID, directory }) => { let response; try { response = await client.session.messages({ sessionID, directory, limit: 100 }); } catch { return null; } const messages = Array.isArray(response?.data) ? response.data : []; let latest = null; for (const message of messages) { const info = message?.info; if (info?.role !== 'assistant' || !Number.isFinite(info?.time?.completed)) continue; if (!latest || (info.time.created || 0) >= (latest.time?.created || 0)) latest = info; } return asNonEmptyString(latest?.id); }; const resolveRequestedDirectory = async ({ payload, readSettingsFromDiskMigrated, sanitizeProjects, validateDirectoryPath }) => { const projectID = asNonEmptyString(payload?.projectId) || asNonEmptyString(payload?.projectID); if (projectID) { const settings = await readSettingsFromDiskMigrated(); const projects = sanitizeProjects(settings?.projects || []); const project = projects.find((entry) => entry.id === projectID) || null; if (!project?.path) { return { ok: false, status: 404, error: 'Project not found' }; } const validated = await validateDirectoryPath(project.path); return validated.ok ? { ok: true, directory: validated.directory, projectId: projectID } : { ok: false, status: 400, error: validated.error || 'Invalid project directory' }; } const directory = asNonEmptyString(payload?.directory); const validated = await validateDirectoryPath(directory); return validated.ok ? { ok: true, directory: validated.directory } : { ok: false, status: 400, error: validated.error || 'Invalid directory' }; }; const PROMPT_LANDED_TIMEOUT_MS = 5_000; const PROMPT_LANDED_POLL_MS = 150; const latestUserMessageID = async ({ client, sessionID, directory }) => { let response; try { response = await client.session.messages({ sessionID, directory, limit: 100 }); } catch { return { ok: false, messageID: null }; } const messages = Array.isArray(response?.data) ? response.data : []; let latest = null; for (const message of messages) { const info = message?.info; if (info?.role !== 'user') continue; if (!latest || (info.time?.created || 0) >= (latest.time?.created || 0)) latest = info; } return { ok: true, messageID: asNonEmptyString(latest?.id) }; }; // `prompt_async` answers 204 as soon as OpenCode forks the run, and every later // failure is reported only on the session event stream. Confirm the prompt was // actually recorded so `promptDispatched` never claims a dispatch that vanished. const waitForPromptLanded = async ({ client, sessionID, directory, baselineUserMessageID }) => { const deadline = Date.now() + PROMPT_LANDED_TIMEOUT_MS; for (;;) { const latest = await latestUserMessageID({ client, sessionID, directory }); // A failed lookup is not authoritative evidence that the prompt was lost. if (!latest.ok) return true; if (latest.messageID && latest.messageID !== baselineUserMessageID) return true; if (Date.now() >= deadline) return false; await new Promise((resolve) => setTimeout(resolve, PROMPT_LANDED_POLL_MS)); } }; const resolveWorktreeInput = (payload) => { if (!payload?.worktree || typeof payload.worktree !== 'object') return null; const name = asNonEmptyString(payload.worktree.name); if (!name) return null; const branchName = asNonEmptyString(payload.worktree.branchName); const startRef = asNonEmptyString(payload.worktree.startRef); return { mode: 'new', name, ...(branchName ? { branchName } : {}), ...(startRef ? { startRef } : {}), ...(typeof payload.setUpstream === 'boolean' ? { setUpstream: payload.setUpstream } : {}), }; }; export const createOpenChamberSessionService = (dependencies) => { const { readSettingsFromDiskMigrated, sanitizeProjects, validateDirectoryPath, buildOpenCodeUrl, getOpenCodeAuthHeaders, waitForOpenCodeReady, emitSessionCreatedEvent, createSessionGoal: createSessionGoalOverride, } = dependencies; // Last user message of an existing session, as a selection to reuse. Returns // null when the session has no user message carrying a model. const fetchLastUserSelection = async ({ client, sessionID, directory }) => { try { const response = await client.session.messages({ sessionID, directory, limit: 20 }); const records = Array.isArray(response?.data) ? response.data : []; for (let index = records.length - 1; index >= 0; index -= 1) { const info = records[index]?.info; if (info?.role !== 'user') continue; const providerID = asNonEmptyString(info.model?.providerID); const modelID = asNonEmptyString(info.model?.modelID); if (!providerID || !modelID) continue; return { model: { providerID, modelID }, agent: asNonEmptyString(info.agent), variant: asNonEmptyString(info.model?.variant), }; } } catch { } return null; }; // Explicit model/agent/variant are never checked by `prompt_async`: an unknown // agent makes the forked run fail silently, leaving a session with no message. // Reject them before any session, worktree, or goal side effect happens. const validateRequestedSelection = async ({ directory, requestedModel, requestedAgent, requestedVariant }) => { if (!requestedModel && !requestedAgent && !requestedVariant) return; const authHeaders = getOpenCodeAuthHeaders(); const { providers, agents } = await fetchSelectionInputs({ buildOpenCodeUrl, authHeaders, directory, readSettingsFromDiskMigrated, }); // An empty list means the lookup failed or returned nothing authoritative; // it must not turn a valid selection into a rejection. if (requestedAgent && agents.length > 0) { const agent = agents.find((entry) => entry?.name === requestedAgent) || null; if (!agent) { throw new OpenChamberControlError(`Unknown agent '${requestedAgent}' for ${directory}`, 400); } if (!isPrimaryAgentMode(agent.mode)) { throw new OpenChamberControlError(`Agent '${requestedAgent}' is a subagent and cannot receive a prompt directly`, 400); } } if (requestedModel && providers.length > 0) { if (!hasProviderModel(providers, requestedModel.providerID, requestedModel.modelID)) { throw new OpenChamberControlError( `Unknown model '${requestedModel.providerID}/${requestedModel.modelID}' for ${directory}`, 400, ); } if (requestedVariant && !resolveVariant(providers, requestedModel.providerID, requestedModel.modelID, requestedVariant)) { throw new OpenChamberControlError( `Unknown variant '${requestedVariant}' for model '${requestedModel.providerID}/${requestedModel.modelID}'`, 400, ); } } }; const dispatchPrompt = async ({ client, baseUrl, authHeaders, sessionID, directory, prompt, goalInput, requestedModel, requestedAgent, requestedVariant, reuseSessionSelection = false, }) => { let model = requestedModel; let agent = requestedAgent; let variant = requestedVariant; if (reuseSessionSelection && (!model || !agent)) { const previous = await fetchLastUserSelection({ client, sessionID, directory }); if (previous) { if (!model && previous.model) { model = previous.model; if (variant == null) variant = previous.variant ?? undefined; } if (!agent && previous.agent) agent = previous.agent; } } if (!model || !agent) { const inputs = await fetchSelectionInputs({ buildOpenCodeUrl, authHeaders, directory, readSettingsFromDiskMigrated, }); const defaults = resolveDefaultSelection(inputs); if (!model) { model = defaults.model; if (variant == null) variant = defaults.variant; } agent = agent || defaults.agent; } if (!model) { const error = new Error('No model is configured or available for the requested directory'); error.statusCode = 400; throw error; } const expandedPrompt = expandSnippets(prompt, directory); const parsedCommand = parseScheduledCommandPrompt(prompt); let resolvedCommand = null; if (parsedCommand) { try { const response = await client.command.list({ directory }); const commands = Array.isArray(response?.data) ? response.data : []; const command = commands.find((candidate) => candidate?.name === parsedCommand.command); if (command) resolvedCommand = { ...parsedCommand, template: command.template }; } catch { } } if (goalInput.enabled) { const commandObjective = resolvedCommand ? expandCommandGoalObjective(resolvedCommand.template, resolvedCommand.arguments) : null; await (createSessionGoalOverride || createSessionGoal)({ baseUrl, authHeaders, sessionID, directory, objective: commandObjective ?? expandedPrompt, tokenBudget: goalInput.tokenBudget, providerID: model.providerID, modelID: model.modelID, onWarning: (message, error) => console.warn(`[OpenChamberSessions] ${message}:`, error?.message || error), }); } const markGoalPartial = (error) => { if (goalInput.enabled && error && typeof error === 'object') error.goalConfigured = true; return error; }; if (resolvedCommand) { try { await client.session.command({ sessionID, directory, command: resolvedCommand.command, arguments: resolvedCommand.arguments, ...(agent ? { agent } : {}), model: `${model.providerID}/${model.modelID}`, ...(variant ? { variant } : {}), }); } catch (error) { throw markGoalPartial(error); } } else { const baseline = await latestUserMessageID({ client, sessionID, directory }); try { await runPromptAsync({ baseUrl, authHeaders, sessionID, directory, payload: { model, ...(agent ? { agent } : {}), ...(variant ? { variant } : {}), parts: [ { type: 'text', text: expandedPrompt }, ...(goalInput.enabled ? [{ type: 'text', text: buildGoalIntroText(goalInput.tokenBudget), synthetic: true }] : []), ], }, }); } catch (error) { throw markGoalPartial(error); } const landed = await waitForPromptLanded({ client, sessionID, directory, baselineUserMessageID: baseline.messageID, }); if (!landed) { return { model, agent, variant, promptDispatched: false, dispatchedAsCommand: false, promptError: 'OpenCode accepted the prompt but it never appeared in the session', }; } } return { model, agent, variant, promptDispatched: true, dispatchedAsCommand: Boolean(resolvedCommand) }; }; const create = async (payload = {}) => { const title = asNonEmptyString(payload.title); const prompt = asNonEmptyString(payload.prompt); const goalInput = resolveGoalInput(payload, prompt); if (!goalInput.ok) { throw new OpenChamberControlError(goalInput.error, 400); } const model = resolveRequestedModel(payload); const agent = asNonEmptyString(payload.agent); const variant = asNonEmptyString(payload.variant); const resolvedDirectory = await resolveRequestedDirectory({ payload, readSettingsFromDiskMigrated, sanitizeProjects, validateDirectoryPath, }); if (!resolvedDirectory.ok) { throw new OpenChamberControlError(resolvedDirectory.error, resolvedDirectory.status || 400); } const worktreeInput = resolveWorktreeInput(payload); let worktree = null; let sessionDirectory = resolvedDirectory.directory; if (payload?.worktree && !worktreeInput) { throw new OpenChamberControlError('worktree.name is required when worktree is provided', 400); } if (typeof waitForOpenCodeReady === 'function') await waitForOpenCodeReady(10_000, 250); if (prompt) { await validateRequestedSelection({ directory: resolvedDirectory.directory, requestedModel: model, requestedAgent: agent, requestedVariant: variant, }); } if (worktreeInput) { worktree = await createWorktree(resolvedDirectory.directory, worktreeInput); sessionDirectory = worktree.path; } const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, ''); const authHeaders = getOpenCodeAuthHeaders(); const client = createOpencodeClient({ baseUrl, headers: authHeaders }); const sessionID = await createSession({ client, baseUrl, authHeaders, directory: sessionDirectory, ...(title ? { title } : {}), }); let dispatch = { model, agent, variant, promptDispatched: false, dispatchedAsCommand: false }; if (prompt) { dispatch = await dispatchPrompt({ client, baseUrl, authHeaders, sessionID, directory: sessionDirectory, prompt, goalInput, requestedModel: model, requestedAgent: agent, requestedVariant: variant, }); } const result = { sessionId: sessionID, directory: sessionDirectory, ...(resolvedDirectory.projectId ? { projectId: resolvedDirectory.projectId } : {}), ...(title ? { title } : {}), ...(worktree ? { worktree } : {}), ...(prompt && dispatch.model ? { model: dispatch.model } : {}), ...(prompt && dispatch.agent ? { agent: dispatch.agent } : {}), ...(prompt && dispatch.variant ? { variant: dispatch.variant } : {}), promptDispatched: dispatch.promptDispatched, ...(dispatch.promptError ? { promptError: dispatch.promptError } : {}), dispatchedAsCommand: dispatch.dispatchedAsCommand, ...(goalInput.enabled ? { goalEnabled: true } : {}), ...(goalInput.tokenBudget ? { goalTokenBudget: goalInput.tokenBudget } : {}), }; try { emitSessionCreatedEvent?.({ sessionID, directory: sessionDirectory, ...(resolvedDirectory.projectId ? { projectID: resolvedDirectory.projectId } : {}), ...(title ? { title } : {}), ...(worktree ? { worktree } : {}), ...(prompt && dispatch.model ? { model: dispatch.model } : {}), ...(prompt && dispatch.agent ? { agent: dispatch.agent } : {}), ...(prompt && dispatch.variant ? { variant: dispatch.variant } : {}), promptDispatched: dispatch.promptDispatched, dispatchedAsCommand: dispatch.dispatchedAsCommand, ...(goalInput.enabled ? { goalEnabled: true } : {}), ...(goalInput.tokenBudget ? { goalTokenBudget: goalInput.tokenBudget } : {}), createdAt: Date.now(), }); } catch { } return result; }; const runExisting = async (action, sourceSessionId, payload = {}) => { const sourceSessionID = asNonEmptyString(sourceSessionId); const prompt = asNonEmptyString(payload.prompt); if (!sourceSessionID) throw new OpenChamberControlError('sessionId is required', 400); if (!prompt) throw new OpenChamberControlError('prompt is required', 400); const goalInput = resolveGoalInput(payload, prompt); if (!goalInput.ok) throw new OpenChamberControlError(goalInput.error, 400); const requestedModel = resolveRequestedModel(payload); let targetSessionID = sourceSessionID; let targetSession = null; let directory = null; try { const resolvedDirectory = await resolveRequestedDirectory({ payload, readSettingsFromDiskMigrated, sanitizeProjects, validateDirectoryPath, }); if (!resolvedDirectory.ok) { throw new OpenChamberControlError(resolvedDirectory.error, resolvedDirectory.status || 400); } directory = resolvedDirectory.directory; if (typeof waitForOpenCodeReady === 'function') await waitForOpenCodeReady(10_000, 250); await validateRequestedSelection({ directory, requestedModel, requestedAgent: asNonEmptyString(payload.agent), requestedVariant: asNonEmptyString(payload.variant), }); const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, ''); const authHeaders = getOpenCodeAuthHeaders(); const client = createOpencodeClient({ baseUrl, headers: authHeaders }); if (action === 'fork') { targetSession = await forkSession({ client, sessionID: sourceSessionID, directory, messageID: asNonEmptyString(payload.messageId) || undefined, }); targetSessionID = targetSession.id; } const baselineAssistantMessageId = await latestCompletedAssistantMessageID({ client, sessionID: targetSessionID, directory, }); const dispatch = await dispatchPrompt({ client, baseUrl, authHeaders, sessionID: targetSessionID, directory, prompt, goalInput, requestedModel, requestedAgent: asNonEmptyString(payload.agent), requestedVariant: asNonEmptyString(payload.variant), reuseSessionSelection: true, }); const result = { action, sessionId: targetSessionID, directory, ...(action === 'fork' ? { sourceSessionId: sourceSessionID } : {}), ...(targetSession?.title ? { title: targetSession.title } : {}), ...(baselineAssistantMessageId ? { baselineAssistantMessageId } : {}), model: dispatch.model, ...(dispatch.agent ? { agent: dispatch.agent } : {}), ...(dispatch.variant ? { variant: dispatch.variant } : {}), promptDispatched: dispatch.promptDispatched, ...(dispatch.promptError ? { promptError: dispatch.promptError } : {}), dispatchedAsCommand: dispatch.dispatchedAsCommand, ...(goalInput.enabled ? { goalEnabled: true } : {}), ...(goalInput.tokenBudget ? { goalTokenBudget: goalInput.tokenBudget } : {}), }; if (action === 'fork') { try { emitSessionCreatedEvent?.({ sessionID: targetSessionID, directory, sourceSessionID, ...(targetSession?.title ? { title: targetSession.title } : {}), model: dispatch.model, ...(dispatch.agent ? { agent: dispatch.agent } : {}), ...(dispatch.variant ? { variant: dispatch.variant } : {}), promptDispatched: dispatch.promptDispatched, dispatchedAsCommand: dispatch.dispatchedAsCommand, ...(goalInput.enabled ? { goalEnabled: true } : {}), ...(goalInput.tokenBudget ? { goalTokenBudget: goalInput.tokenBudget } : {}), createdAt: Date.now(), }); } catch { } } return result; } catch (error) { const statusCode = Number(error?.statusCode) || 500; const forkCreated = action === 'fork' && targetSessionID !== sourceSessionID; const goalConfigured = error?.goalConfigured === true; throw new OpenChamberControlError( error instanceof Error ? error.message : `Failed to ${action} session`, statusCode, { ...(forkCreated || goalConfigured ? { partial: true, partialAction: forkCreated ? 'fork-created' : 'goal-configured', sessionId: targetSessionID, directory, } : {}), }, ); } }; return { create, send: (sessionID, payload) => runExisting('send', sessionID, payload), fork: (sessionID, payload) => runExisting('fork', sessionID, payload), }; }; const sendServiceError = (res, error, fallback) => { const controlError = asControlError(error, fallback); return res.status(controlError.statusCode).json({ error: controlError.message, ...(controlError.partial === true ? { partial: true, partialAction: controlError.partialAction, sessionId: controlError.sessionId, directory: controlError.directory, } : {}), }); }; export const registerOpenChamberSessionRoutes = (app, dependencies) => { const service = dependencies.sessionService || createOpenChamberSessionService(dependencies); app.post('/api/openchamber/sessions', express.json({ limit: '1mb' }), async (req, res) => { try { return res.json(await service.create(req.body && typeof req.body === 'object' ? req.body : {})); } catch (error) { console.error('[OpenChamberSessions] failed to create session:', error); return sendServiceError(res, error, 'Failed to create session'); } }); app.post( '/api/openchamber/sessions/:sessionId/send', express.json({ limit: '1mb' }), async (req, res) => { try { return res.json(await service.send(req.params.sessionId, req.body)); } catch (error) { console.error('[OpenChamberSessions] failed to send session:', error); return sendServiceError(res, error, 'Failed to send session'); } }, ); app.post( '/api/openchamber/sessions/:sessionId/fork', express.json({ limit: '1mb' }), async (req, res) => { try { return res.json(await service.fork(req.params.sessionId, req.body)); } catch (error) { console.error('[OpenChamberSessions] failed to fork session:', error); return sendServiceError(res, error, 'Failed to fork session'); } }, ); };