openvibers/openvibe.codes Commits

Each turn can run at the effort a person picks, in each agent's own words (L31 CH2) (#204)

OP OpenVibers committed 13 days ago · 0535a814fb · 12 files, +121 −20

The chat's composer offers the effort in plain words (Quick, Balanced, Thorough, Deepest) on a new chat and on a
carry-on; the job input keeps a level on the shared ladder; each CLI gets it its own way (Claude Code and Command Code
--effort, Codex model_reasoning_effort, Aider --reasoning-effort, OpenCode none), fitted to the levels it accepts. The
attempt keeps it (expand migration 0021), its events carry it, and the thread says what each turn ran with. The JSON
continue takes model and effort too.

Changes

ddiff --git a/engine/adapters/base.js b/engine/adapters/base.js
iindex 5ecb026..ecb5e06 100644
---- a/engine/adapters/base.js
++++ b/engine/adapters/base.js
@@@ -64,6 +64,7 @@ const EFFORT_LEVELS = {
codex: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'],
'command-code': ['off', 'low', 'high', 'max'],
opencode: [],
++ aider: ['low', 'medium', 'high'],
'minimax-code': ['low', 'medium', 'high', 'xhigh', 'max'],
};
ddiff --git a/harness/adapters/cli.js b/harness/adapters/cli.js
iindex 2d986e8..035b745 100644
---- a/harness/adapters/cli.js
++++ b/harness/adapters/cli.js
@@@ -5,7 +5,8 @@
* and how to read what it prints into the shared events (harness/events.js):
*
* { id, name, kind: 'cli', bin, start(task, ctx) } ctx: { env } (the harness's environment: PATH finds the CLI)
-- * task: { prompt, cwd, model, resume, permission: 'read' | 'edit' | 'full', maxTurns, timeoutMs, signal }
++ * task: { prompt, cwd, model, effort, resume, permission: 'read' | 'edit' | 'full', maxTurns, timeoutMs, signal }
++ * effort is the shared ladder (low … max); each CLI gets the nearest level it accepts, or none (OpenCode has no flag)
*
* Permission levels mean the same for every agent, mapped onto what each CLI offers:
* read look and answer: no file is changed and no command that changes anything runs
@@@ -62,6 +63,7 @@ const claudeCode = {
const mode = o.permissionMode || { read: 'plan', edit: 'acceptEdits', full: 'bypassPermissions' }[t.permission] || 'plan';
const args = ['-p', t.prompt, '--output-format', 'stream-json', '--verbose', '--permission-mode', mode];
if (t.model) args.push('--model', t.model);
++ if (t.effort) args.push('--effort', effortFor('claude-code', t.effort));
if (t.resume) args.push('--resume', t.resume);
if (t.maxTurns) args.push('--max-turns', String(t.maxTurns));
// MCP servers this session may use (the conductor's Codes tools): a config file, and the tools it may call
@@@ -95,6 +97,7 @@ const codex = {
? ['exec', 'resume', t.resume, '--json', '--skip-git-repo-check']
: ['exec', '--json', '--skip-git-repo-check', '-C', t.cwd, '-s', sandbox];
if (t.model) args.push('-m', t.model);
++ if (t.effort) args.push('-c', `model_reasoning_effort="${effortFor('codex', t.effort)}"`);
args.push(t.prompt);
const startedAt = Date.now();
const parser = jsonLines(codex, (e, emit, st) => {
@@@ -191,6 +194,7 @@ const commandCode = {
const args = [`--print=${t.prompt}`, '--output-format', 'json', '--skip-onboarding', '--no-auto-update', '-t'];
if (t.permission !== 'read') args.push('--yolo');
if (t.model) args.push('-m', t.model);
++ if (t.effort) args.push('--effort', effortFor('command-code', t.effort));
if (t.resume) args.push('--resume', t.resume);
if (t.maxTurns) args.push('--max-turns', String(t.maxTurns));
const startedAt = Date.now();
@@@ -227,6 +231,7 @@ const aider = {
const args = ['--message', t.prompt, '--yes-always', '--no-pretty', '--no-stream', '--no-check-update', '--no-show-release-notes'];
if (t.permission === 'read') args.push('--dry-run');
if (t.model) args.push('--model', t.model);
++ if (t.effort) args.push('--reasoning-effort', effortFor('aider', t.effort));
const startedAt = Date.now();
const lines = [];
let started = false;
ddiff --git a/harness/index.js b/harness/index.js
iindex d50c6a3..67543cb 100644
---- a/harness/index.js
++++ b/harness/index.js
@@@ -154,7 +154,7 @@ function createHarness({ env = process.env, fetch: fetchImpl = globalThis.fetch,
return decision;
}
-- function run({ prompt, task = 'edit', agent = null, model = null, cwd = process.cwd(), permission = 'edit', resume = null, handoff = true, maxAttempts = 3, stallMs = 10 * 60_000, timeoutMs, maxTurns, requirements = {}, signal, adapterOptions = null } = {}) {
++ function run({ prompt, task = 'edit', agent = null, model = null, effort = null, cwd = process.cwd(), permission = 'edit', resume = null, handoff = true, maxAttempts = 3, stallMs = 10 * 60_000, timeoutMs, maxTurns, requirements = {}, signal, adapterOptions = null } = {}) {
if (!prompt || !String(prompt).trim()) throw new Error('a run needs a prompt');
if (!['read', 'edit', 'full'].includes(permission)) throw new Error('permission is read, edit or full');
const runId = store.newRunId();
@@@ -200,7 +200,7 @@ function createHarness({ env = process.env, fetch: fetchImpl = globalThis.fetch,
const controller = new AbortController();
if (signal) signal.addEventListener('abort', () => controller.abort(), { once: true });
current = adapter.start({
-- prompt: nextPrompt, cwd, model: modelFor, resume: attempt === 1 ? resume : null, permission, maxTurns, timeoutMs, signal: controller.signal,
++ prompt: nextPrompt, cwd, model: modelFor, effort, resume: attempt === 1 ? resume : null, permission, maxTurns, timeoutMs, signal: controller.signal,
// Extra flags a caller needs (the conductor's MCP config and permission rules); the adapter decides
// which of them its CLI understands. Nothing here can replace the fields above.
...(adapterOptions && typeof adapterOptions === 'object' ? { options: adapterOptions } : {}),
ddiff --git a/harness/job.js b/harness/job.js
iindex 4560780..136a838 100644
---- a/harness/job.js
++++ b/harness/job.js
@@@ -100,6 +100,8 @@ function parseArgs(raw, env = process.env) {
let a;
try { a = JSON.parse(String(raw || '').trim() || '{}'); } catch { throw new Error('the job input is not JSON'); }
if (!a || typeof a !== 'object' || Array.isArray(a)) throw new Error('the job input must be an object');
++ const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
++ const effort = EFFORTS.includes(a.effort) ? a.effort : null;
const kind = a.kind === 'bench' ? 'bench' : a.kind === 'conductor' ? 'conductor' : 'code';
const prompt = typeof a.prompt === 'string' ? a.prompt.trim() : '';
if (!prompt || prompt.length > MAX_PROMPT) throw new Error(`prompt must be 1-${MAX_PROMPT} characters`);
@@@ -124,6 +126,7 @@ function parseArgs(raw, env = process.env) {
folder, safety, mcp: a.mcp === true, codesUrl,
taskId: typeof a.task_id === 'string' && /^tsk_[0-9A-HJKMNP-TV-Z]{26}$/.test(a.task_id) ? a.task_id : null,
model: typeof a.model === 'string' && a.model.length <= 100 ? a.model : null,
++ ...(effort ? { effort } : {}),
agent: 'claude-code',
...(files.length ? { attachments: files } : {}),
...(resume ? { resume } : {}),
@@@ -165,6 +168,7 @@ function parseArgs(raw, env = process.env) {
...(Object.keys(wanted).length ? { env: wanted } : {}),
agent: typeof a.agent === 'string' && /^[a-z0-9-]{1,40}$/.test(a.agent) ? a.agent : null,
model: typeof a.model === 'string' && a.model.length <= 100 ? a.model : null,
++ ...(effort ? { effort } : {}),
};
}
@@@ -490,7 +494,7 @@ async function runJob({ input, harness, emit, writeResult, env = process.env, tm
say({ t: 'step', text: 'Preparing an empty benchmark workspace' });
}
const run = harness.run({
-- prompt: job.prompt, task: 'edit', agent: job.agent, model: job.model, cwd: work,
++ prompt: job.prompt, task: 'edit', agent: job.agent, model: job.model, effort: job.effort || null, cwd: work,
permission: job.permission, handoff: !job.agent, maxAttempts: job.kind === 'conductor' ? 1 : 3,
// A task carried on in the same conversation resumes its session (RESUME in the environment, or the job's
// own resume field).
ddiff --git a/migrations/0021_attempt_effort.sql b/migrations/0021_attempt_effort.sql
nnew file mode 100644
iindex 0000000..ddba11c
---- /dev/null
++++ b/migrations/0021_attempt_effort.sql
@@@ -0,0 +1,5 @@
++-- phase: expand
++-- Codes L31 CH2: the reasoning effort a person chose for a turn (low, medium, high, xhigh or max on the shared ladder;
++-- each agent gets the nearest level it accepts). Kept on the attempt beside its model, so the conversation can say
++-- what each turn ran with. NULL is the agent's own default. No prompt, key or path is kept here.
++ALTER TABLE codes_attempts ADD COLUMN IF NOT EXISTS effort text COLLATE "C";
ddiff --git a/server/domain/attempts.js b/server/domain/attempts.js
iindex 9555279..f673dab 100644
---- a/server/domain/attempts.js
++++ b/server/domain/attempts.js
@@@ -150,18 +150,18 @@ function createAttempts({ store, config = {}, log = console, harnesses = null })
* One attempt. The fence: follow_up_of is unique, so an ending that starts a follow-up twice answers the same
* attempt the second time and starts no second job. created tells the caller which of the two it was.
*/
-- async function create({ task, agent = null, model = null, machine = null, session = null, why = null, kind = 'first', followUpOf = null, state = 'queued', budget = null }) {
++ async function create({ task, agent = null, model = null, effort = null, machine = null, session = null, why = null, kind = 'first', followUpOf = null, state = 'queued', budget = null }) {
if (!task || !task.id) throw new Error('an attempt belongs to a task');
for (let tries = 0; tries < 5; tries++) {
const n = await db.one('SELECT COALESCE(MAX(n), 0) + 1 AS n FROM codes_attempts WHERE task_id = $1', [task.id]);
const id = store.newId('att');
const inserted = await db.maybe(
-- `INSERT INTO codes_attempts (id, task_id, n, agent, model, machine, session, state, why, kind, follow_up_of, budget_usd, budget_tokens, created_at)
-- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14::timestamptz)
++ `INSERT INTO codes_attempts (id, task_id, n, agent, model, machine, session, state, why, kind, follow_up_of, budget_usd, budget_tokens, created_at, effort)
++ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14::timestamptz, $15)
ON CONFLICT DO NOTHING RETURNING *`,
[id, task.id, n.n, agent, model, machine, session, state, why, kind, followUpOf,
budget && Number(budget.usd) > 0 ? Number(budget.usd) : null,
-- budget && Number(budget.tokens) > 0 ? Math.round(Number(budget.tokens)) : null, store.iso()],
++ budget && Number(budget.tokens) > 0 ? Math.round(Number(budget.tokens)) : null, store.iso(), effort || null],
);
if (inserted) {
await db.exec('UPDATE run_tasks SET attempts_count = $2 WHERE id = $1', [task.id, n.n]);
ddiff --git a/server/domain/run-tasks.js b/server/domain/run-tasks.js
iindex 5dad011..f1ba425 100644
---- a/server/domain/run-tasks.js
++++ b/server/domain/run-tasks.js
@@@ -558,7 +558,7 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
// The budget of the attempt: the task's limits (a benchmark's per-task limits) or the person's default.
const budget = budgetFor(runArgs.limits);
const first = await attemptStore.create({
-- task: inserted, agent: agent || null, model: model || null, why,
++ task: inserted, agent: agent || null, model: model || null, effort: runArgs.effort || null, why,
kind: 'first', state: 'queued', budget,
});
let job;
@@@ -796,7 +796,7 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
* continue after a crash, a wrap-up, another manual press — that attempt is returned, and no second one is started
* beside it. Two sessions on one worktree would fight over the same files.
*/
-- async function continueTask(row, { note = null, agent = null } = {}) {
++ async function continueTask(row, { note = null, agent = null, model = null, effort = null } = {}) {
if (!row || !row.id) return { error: 'no such task' };
const attempts = await attemptStore.list(row.id);
const ending = attempts[attempts.length - 1];
@@@ -806,12 +806,12 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
const text = note ? String(note).trim().slice(0, 4000) : '';
const plan = agent
? {
-- kind: 'switch', agent, model: null, session: null,
++ kind: 'switch', agent, model: model || null, effort: effort || null, session: null,
prompt: `${row.prompt}\n\n---\n## Hand-over (written by openvibe.codes)\n\n${attemptStore.nameOf(agent)} takes over from ${attemptStore.nameOf(ending.agent)}, which ${attemptStore.wordsOf(ending.reason || 'error')}. Keep the work that was done, do not redo it, and finish the task.\n`,
why: `${attemptStore.nameOf(agent)} took over at the owner's request`,
}
: {
-- kind: 'manual', agent: ending.agent, model: ending.model, session: null,
++ kind: 'manual', agent: ending.agent, model: model || ending.model, effort: effort || ending.effort || null, session: null,
prompt: `${row.prompt}${text ? `\n\n---\n## A note from the owner\n\n${text}\n` : ''}`,
why: text ? `carried on by the owner: ${text.slice(0, 200)}` : 'carried on by the owner',
};
@@@ -835,7 +835,7 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
if (!TERMINAL.has(current.state)) return { error: 'the task is still running' };
if (current.outcome === 'stopped') return { error: 'the task was stopped on purpose' };
const made = await attemptStore.create({
-- task: current, agent: plan.agent, model: plan.model, session: null, why: plan.why, kind: plan.kind, state: 'queued', budget: left.left,
++ task: current, agent: plan.agent, model: plan.model, effort: plan.effort || null, session: null, why: plan.why, kind: plan.kind, state: 'queued', budget: left.left,
});
attempt = made.attempt;
await db.one(`UPDATE run_tasks SET state = 'queued', outcome = NULL, outcome_note = NULL, error = NULL, job_id = NULL, finished_at = NULL, updated_at = $2 WHERE id = $1 RETURNING *`,
@@@ -869,6 +869,8 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
return parseArgs(JSON.stringify({
repo: row.repo, ref: row.ref || null, prompt: plan.prompt || row.prompt,
agent: plan.agent || null, permission: 'edit',
++ ...(plan.model ? { model: plan.model } : {}),
++ ...(plan.effort ? { effort: plan.effort } : {}),
...(row.push ? { push: { pull_request: !!row.pull_request } } : {}),
...(plan.session ? { resume: plan.session } : {}),
}));
ddiff --git a/server/http/chat.js b/server/http/chat.js
iindex 39f312a..7264cc5 100644
---- a/server/http/chat.js
++++ b/server/http/chat.js
@@@ -202,7 +202,8 @@ ${rows.length ? '' : '<p class="ch-empty-rail">Your conversations appear here.</
const answer = (done && done.text) || said[said.length - 1] || '';
const along = said.slice(0, answer && said[said.length - 1] === answer ? -1 : undefined);
const alongSteps = along.map((text) => ({ name: 'Said', what: firstLine(text, 140), kind: 'sub', ok: null }));
-- const who = `<div class="ch-who">${avatar(a)}<b>${esc(a.name)}</b>${attempt && attempt.model ? `<span>${esc(attempt.model)}</span>` : ''}</div>`;
++ const ran = attempt ? [attempt.model, attempt.effort ? effortWord(attempt.effort) : null].filter(Boolean).join(' · ') : '';
++ const who = `<div class="ch-who">${avatar(a)}<b>${esc(a.name)}</b>${ran ? `<span>${esc(ran)}</span>` : ''}</div>`;
const body = answer ? `<div class="ch-md">${markdown(answer)}</div>` : live ? '' : `<p class="ch-quiet">${esc(attempt && attempt.end_why ? attempt.end_why : 'It ended without a word.')}</p>`;
const working = live ? `<div class="ch-working" data-ch-working><span class="ch-dots" aria-hidden="true"><i></i><i></i><i></i></span><span data-ch-working-text>${steps.length ? esc(`${steps[steps.length - 1].name} ${steps[steps.length - 1].what || ''}`.trim()) : 'Starting…'}</span></div>` : '';
return `<div class="ch-turn ch-agent${live ? ' is-live' : ''}"${attempt ? ` data-attempt="${Number(attempt.n)}"` : ''}>${who}${stepsHtml([...alongSteps, ...steps], ms, live)}${body}${working}</div>`;
@@@ -295,6 +296,12 @@ ${rows.length ? '' : '<p class="ch-empty-rail">Your conversations appear here.</
const list = harnesses?.list?.() || [];
return `<label class="ch-pick" title="${esc(label)}"><span class="ch-sr">${esc(label)}</span><select name="agent" data-ch-agent><option value="">${esc(auto)}</option>${list.map((a) => `<option value="${esc(a.id)}"${selected === a.id ? ' selected' : ''}>${esc(a.name)}</option>`).join('')}</select>${ico('chevron', 'ch-caret')}</label>`;
}
++ // The effort in plain words (the shared ladder underneath; each agent gets the nearest level it accepts).
++ const EFFORT = [['', 'Effort: auto'], ['low', 'Quick'], ['medium', 'Balanced'], ['high', 'Thorough'], ['max', 'Deepest']];
++ const effortWord = (v) => (EFFORT.find(([k]) => k === v) || [null, v])[1];
++ function effortPicker(selected = '') {
++ return `<label class="ch-pick" title="How hard it thinks"><span class="ch-sr">Effort</span>${ico('sub')}<select name="effort" data-ch-effort>${EFFORT.map(([v, l]) => `<option value="${v}"${selected === v ? ' selected' : ''}>${l}</option>`).join('')}</select>${ico('chevron', 'ch-caret')}</label>`;
++ }
function composer({ action, api = '', placeholder, pickers = '', live = false, stopAction = '', hidden = '', note = '' }) {
return `<form class="ch-composer" method="post" action="${esc(action)}"${api ? ` data-ch-api="${esc(api)}"` : ''} data-ch-composer>${hidden}
<label class="ch-sr" for="ch-text">${esc(placeholder)}</label>
@@@ -349,7 +356,7 @@ ${facts.length ? `<section><p class="ch-group">Where</p><ul class="ch-facts">${f
const error = String(req.query.error || '').slice(0, 200);
const name = String(req.viewer.displayName || req.viewer.username || '').split(' ')[0];
const ideas = ['Fix the failing tests', 'Explain how this project is laid out', 'Make the home page faster on phones', 'Find and fix one bug'];
-- const pickers = `<label class="ch-pick ch-pick-repo" title="Repository"><span class="ch-sr">Repository</span>${ico('repo')}<input name="repo" type="url" value="${esc(repo)}" placeholder="Repository URL" list="ch-repos" autocomplete="url" required data-ch-repo data-ch-repo-url="/api/v1/me/repos"><datalist id="ch-repos">${repos.map((x) => `<option value="${esc(x.url)}">${esc(x.full_name)}</option>`).join('')}</datalist></label>${agentPicker(settings.default_agent || '')}`;
++ const pickers = `<label class="ch-pick ch-pick-repo" title="Repository"><span class="ch-sr">Repository</span>${ico('repo')}<input name="repo" type="url" value="${esc(repo)}" placeholder="Repository URL" list="ch-repos" autocomplete="url" required data-ch-repo data-ch-repo-url="/api/v1/me/repos"><datalist id="ch-repos">${repos.map((x) => `<option value="${esc(x.url)}">${esc(x.full_name)}</option>`).join('')}</datalist></label>${agentPicker(settings.default_agent || '')}${effortPicker('')}`;
const thread = `<div class="ch-hello"><span class="ov-mark" data-size="56" aria-hidden="true"></span><h1>${name ? `What should we build, ${esc(name)}?` : 'What should we build?'}</h1><p>Say it the way you would to a colleague. Codes picks the agent, runs it on your machine, and shows you what changed.</p>${error ? `<p class="ch-error" role="alert">${esc(error)}</p>` : ''}<div class="ch-ideas" aria-label="Ideas">${ideas.map((t) => `<button class="ch-chip" type="button" data-ch-idea="${esc(t)}">${esc(t)}</button>`).join('')}</div>${repos.length > 1 ? `<div class="ch-ideas ch-recent" aria-label="Your recent repositories">${repos.map((x) => `<button class="ch-chip" type="button" data-ch-saved-repo="${esc(x.url)}">${ico('repo')}${esc(x.full_name)}</button>`).join('')}</div>` : ''}</div>`;
return frame(req, res, {
title: 'New chat', empty: true, head: '<h1 class="ch-title">New chat</h1>', thread,
@@@ -370,7 +377,7 @@ ${facts.length ? `<section><p class="ch-group">Where</p><ul class="ch-facts">${f
let args;
try {
const { parseArgs } = require('../../harness/job');
-- args = parseArgs(JSON.stringify({ repo: String(b.repo || '').trim(), prompt: text, agent: agent || null, permission: 'edit' }));
++ args = parseArgs(JSON.stringify({ repo: String(b.repo || '').trim(), prompt: text, agent: agent || null, permission: 'edit', ...(b.effort ? { effort: String(b.effort) } : {}) }));
} catch (e) { return back(`Check the repository: ${e.message}.`); }
let project = null;
try { project = (await tasks.latestProject?.(req.viewer.subject)) || await conductor.projectFor(req.viewer.token || null, req.viewer.subject); } catch { project = null; }
@@@ -405,7 +412,7 @@ ${facts.length ? `<section><p class="ch-group">Where</p><ul class="ch-facts">${f
// The stream starts after the last line this page already shows.
const seqRow = live ? await db.maybe('SELECT max(seq) AS seq FROM codes_session_lines WHERE task_id = $1', [row.id]) : null;
const lastSeq = seqRow && seqRow.seq != null ? Number(seqRow.seq) : 0;
-- const pickers = agentPicker('', { label: 'Who carries on', auto: `Same agent (${agentOf(row.agent, agentNames()).name})` });
++ const pickers = agentPicker('', { label: 'Who carries on', auto: `Same agent (${agentOf(row.agent, agentNames()).name})` }) + effortPicker('');
return frame(req, res, {
currentId: row.id, title: titleOf(row), head, side: await sideHtml(row),
thread: (error ? `<p class="ch-error" role="alert">${esc(error)}</p>` : '') + await taskThread(row),
@@@ -428,7 +435,8 @@ ${facts.length ? `<section><p class="ch-group">Where</p><ul class="ch-facts">${f
const b = req.body || {};
const agent = String(b.agent || '').trim() || null;
if (agent && !(harnesses?.list?.() || []).some((a) => a.id === agent)) return res.redirect(303, `/tasks/${encodeURIComponent(row.id)}?error=${encodeURIComponent('Choose an agent from the list.')}`);
-- const out = await tasks.continueTask(row, { note: String(b.text || '').slice(0, 4000), agent });
++ const effort = ['low', 'medium', 'high', 'xhigh', 'max'].includes(b.effort) ? b.effort : null;
++ const out = await tasks.continueTask(row, { note: String(b.text || '').slice(0, 4000), agent, effort });
return res.redirect(303, `/tasks/${encodeURIComponent(row.id)}${out.error ? `?error=${encodeURIComponent(out.error)}` : ''}`);
});
ddiff --git a/server/http/task-events.js b/server/http/task-events.js
iindex b824d55..a769f1e 100644
---- a/server/http/task-events.js
++++ b/server/http/task-events.js
@@@ -28,7 +28,7 @@ function createTaskEvents({ store, config = {}, log = console, attempts = null }
const frame = (event, data, id = null) => (id == null ? '' : `id: ${id}\n`) + `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
const started = (a) => ({
-- type: 'attempt_started', attempt_n: Number(a.n), id: a.id, agent: a.agent || null, model: a.model || null,
++ type: 'attempt_started', attempt_n: Number(a.n), id: a.id, agent: a.agent || null, model: a.model || null, effort: a.effort || null,
why: a.why || null, state: 'queued', at: a.created_at ? new Date(a.created_at).toISOString() : null,
});
const ended = (a) => ({
ddiff --git a/server/http/tasks-api.js b/server/http/tasks-api.js
iindex c1e6363..560716a 100644
---- a/server/http/tasks-api.js
++++ b/server/http/tasks-api.js
@@@ -326,7 +326,10 @@ function createTasksApi({ config, store, keys, run, harnesses, actorLimits, task
const note = typeof b.note === 'string' ? b.note.slice(0, 4000) : null;
const agent = typeof b.agent === 'string' && b.agent ? b.agent : null;
if (agent && !harnesses.list().some((a) => a.id === agent)) return problem(req, res, 422, 'codes.agent_unknown', 'agent is one of GET /api/v1/harnesses');
-- const out = await taskStore.continueTask(row, { note, agent });
++ const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
++ const model = typeof b.model === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:\/#-]{0,99}$/.test(b.model) ? b.model : null;
++ const effort = EFFORTS.includes(b.effort) ? b.effort : null;
++ const out = await taskStore.continueTask(row, { note: note || (typeof b.text === 'string' ? b.text.slice(0, 4000) : null), agent, model, effort });
if (out.error) return problem(req, res, 409, 'codes.task_conflict', out.error);
// One continuation at a time: an attempt that is already live answers this press, and no second one starts.
if (!out.live) log.info(`[Codes] task ${row.id} carried on (${agent ? `with ${agent}` : 'by the owner'})`);
ddiff --git a/test/chat.test.js b/test/chat.test.js
iindex 849deca..6a4f1a9 100644
---- a/test/chat.test.js
++++ b/test/chat.test.js
@@@ -68,6 +68,16 @@ const { stepKind } = require('../server/http/chat');
assert.ok(row && row.prompt === 'Make the footer quieter' && row.subject === ada.subject, 'the brief is the person&rsquo;s own task');
});
++ await check('the effort a person picks rides on the first attempt, and the thread says it in words', async () => {
++ const made = await t.get('/chat', { as: ada, form: { text: 'Think hard about the cache', repo: 'https://github.com/OpenVibers/OpenVibe.Live', effort: 'high' } });
++ assert.strictEqual(made.status, 303);
++ const id = made.headers.get('location').split('/').pop();
++ const att = await db.maybe('SELECT effort FROM codes_attempts WHERE task_id = $1 ORDER BY n LIMIT 1', [id]);
++ assert.ok(att && att.effort === 'high', `the attempt keeps it (${att && att.effort})`);
++ const home = await t.get('/', { as: ada });
++ assert.ok(home.text.includes('name="effort"') && home.text.includes('>Thorough</option>'), 'the composer offers it in plain words');
++ });
++
await check('a visitor who is not signed in gets the product&rsquo;s home, not the app', async () => {
const page = await t.get('/');
assert.strictEqual(page.status, 200);
ddiff --git a/test/harness-effort.test.js b/test/harness-effort.test.js
nnew file mode 100644
iindex 0000000..28c6182
---- /dev/null
++++ b/test/harness-effort.test.js
@@@ -0,0 +1,63 @@
++'use strict';
++/**
++ * The reasoning effort a person picks for a turn (plan L31 CH2) reaches each CLI in its own words: Claude Code and
++ * Command Code take --effort, Codex its model_reasoning_effort setting, Aider --reasoning-effort, and OpenCode has no
++ * such flag. The level is fitted to what each one accepts (engine/adapters/base.js effortFor: Command Code has no
++ * "medium", Aider no "max"), and a turn without an effort passes none. The job input keeps only a level on the ladder.
++ */
++const assert = require('assert');
++const fs = require('fs');
++const os = require('os');
++const path = require('path');
++const { check, done } = require('./helpers/boot');
++const cli = require('../harness/adapters/cli');
++const { parseArgs } = require('../harness/job');
++
++// One stand-in for every CLI: it records its argv and says nothing (the adapter then reports a failed run).
++const FAKE = `
++const fs = require('fs');
++if (process.argv.includes('--version')) { console.log('9.9.9'); process.exit(0); }
++fs.writeFileSync(process.env.FAKE_RECORD, JSON.stringify({ argv: process.argv.slice(2) }));
++`;
++
++(async () => {
++ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-effort-'));
++ const bin = path.join(dir, 'bin');
++ fs.mkdirSync(bin);
++ for (const name of ['claude', 'codex', 'cmd', 'aider', 'opencode']) fs.writeFileSync(path.join(bin, name), `#!${process.execPath}\n'use strict';\n${FAKE}\n`, { mode: 0o755 });
++ const record = path.join(dir, 'record.json');
++ const argvOf = async (adapter, effort) => {
++ fs.rmSync(record, { force: true });
++ for await (const e of adapter.start({ prompt: 'tidy the module', cwd: dir, permission: 'edit', ...(effort ? { effort } : {}) }, { env: { PATH: bin, FAKE_RECORD: record } })) void e;
++ return JSON.parse(fs.readFileSync(record, 'utf8')).argv;
++ };
++ const after = (argv, flag) => { const i = argv.indexOf(flag); return i < 0 ? null : argv[i + 1]; };
++ try {
++ await check('each CLI gets the effort in its own words', async () => {
++ assert.strictEqual(after(await argvOf(cli.claudeCode, 'high'), '--effort'), 'high', 'Claude Code');
++ const codex = await argvOf(cli.codex, 'high');
++ assert.ok(codex.includes('model_reasoning_effort="high"') && after(codex, '-c') === 'model_reasoning_effort="high"', `Codex: ${codex.join(' ')}`);
++ assert.strictEqual(after(await argvOf(cli.commandCode, 'high'), '--effort'), 'high', 'Command Code');
++ assert.strictEqual(after(await argvOf(cli.aider, 'high'), '--reasoning-effort'), 'high', 'Aider');
++ });
++
++ await check('a level a CLI does not have becomes the nearest one it does', async () => {
++ assert.strictEqual(after(await argvOf(cli.commandCode, 'medium'), '--effort'), 'high', 'Command Code has no medium: a tie goes up');
++ assert.strictEqual(after(await argvOf(cli.aider, 'max'), '--reasoning-effort'), 'high', 'Aider tops out at high');
++ });
++
++ await check('no effort, no flag; and OpenCode, which has none, is never given one', async () => {
++ const plain = await argvOf(cli.claudeCode, null);
++ assert.ok(!plain.includes('--effort'), 'Claude Code at its default');
++ assert.ok(!(await argvOf(cli.codex, null)).some((a) => /reasoning_effort/.test(a)), 'Codex at its default');
++ assert.ok(!(await argvOf(cli.opencode, 'max')).some((a) => /effort/.test(a)), 'OpenCode');
++ });
++
++ await check('the job input keeps an effort on the ladder and drops anything else', () => {
++ assert.strictEqual(parseArgs(JSON.stringify({ repo: 'https://github.com/a/b', prompt: 'x', effort: 'max' })).effort, 'max');
++ assert.strictEqual(parseArgs(JSON.stringify({ repo: 'https://github.com/a/b', prompt: 'x', effort: 'ludicrous' })).effort, undefined);
++ assert.strictEqual(parseArgs(JSON.stringify({ kind: 'conductor', folder: '/home/x', prompt: 'x', effort: 'low' })).effort, 'low');
++ });
++ } finally { fs.rmSync(dir, { recursive: true, force: true }); }
++ done();
++})().catch((e) => { console.error(e); process.exit(1); });