Files
openchamber/packages/web/server/lib/openchamber-sessions/routes.js
T
Bohdan Triapitsyn b0028283fb feat: expand slash-command goals from command templates
Resolves armed slash-command objectives from authoritative templates before dispatch
Applies OpenCode argument expansion for goal metadata in UI and scheduled tasks
Falls back to the raw invocation when command details are unavailable
2026-07-29 00:47:29 +03:00

715 lines
26 KiB
JavaScript

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 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;
};
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 {
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);
}
}
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 (worktreeInput) {
worktree = await createWorktree(resolvedDirectory.directory, worktreeInput);
sessionDirectory = worktree.path;
}
if (typeof waitForOpenCodeReady === 'function') await waitForOpenCodeReady(10_000, 250);
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,
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);
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: true,
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: true,
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');
}
},
);
};