openvibers/openvibe.codes Commits
engine/: the agent engine, ported from the portal (C1) (#159)
Changes
nnew file mode 100644@@@ -0,0 +1,176 @@++# engine/++++The agent engine: the pieces of the private agent portal (learned in production, on this workstation, since June 2026)++that every Codes surface shares — the CLI on a Node, the hosted runner and, until it retires, the portal itself.++++CommonJS, Node 22, no dependencies.++++```js++const engine = require('./engine');++++engine.classify('Root cause of the race condition in the webhook'); // { tier: 'hard', why: 'a hard problem' }++engine.outcome(attempts); // { state, reason, step, … }++```++++## The modules++++### `router.js` — what kind of task is this, and which runner may take it++++A brief is classified into one of five tiers (`basic`, `research`, `standard`, `hard`, `design`); a line `tier: hard`++in the brief forces it. Each tier has a ladder, cheapest suitable first: free models for chores, flat-price plans for++everyday code, the strongest models for hard problems and design. `pick()` walks that ladder and skips a runner that is++off, unhealthy, at its slot cap, or under 40% real success over seven days.++++Public: `classify(prompt, opts)`, `classifyText(text, opts)`, `pick(prompt, ctx)`, `track(runs)`, `briefFor(prompt, JOBS, name)`,++`forcedTier(text)`, `LADDER`, `TIERS`.++++```js++const { classify, pick } = require('./engine/router');++++classify('tier: hard\nFind the race condition'); // { tier: 'hard', why: 'the brief says so' }++++pick('Fix the parser', { // → { runner: 'mm', model: null, effort: 'high', … }++ cfg: { subagents: { mm: { enabled: true, max: 2 } } },++ health: { mm: { state: 'ok' } },++ counts: {}, models: {}, runs: [],++});++```++++The ladder is the portal's, so the portal's jobs keep their routing. Codes supplies its own through++`pick(prompt, { ladder })`.++++### `chain.js` — the attempts of one task, why they stopped, what happens next++++Pure: it takes the attempts and returns the outcome and the next step. It never reads a log folder, a marker file or a++process table — the caller passes the data in. The failure reasons are the portal's: `limit`, `auth`, `capacity`,++`refused`, `error`, `budget`, `stopped`, `crash`, `timeout`, `incomplete`, `context`, `hung`.++++An attempt is `{ key, runner, name, runnerName, status, updated, error, next, prev, parent, resolvedNote, stopped, budget, wrapUp, facts }`.++++Public: `outcome(attempts, opts)`, `nextStep(attempts, opts)`, `annotate(attempts)`, `settle(attempts, start)`,++`nextRunner(run, ctx)`, `detect(attempt)`, `facts(attempt, events)`, `continuationNote(run, logFile)`,++`reason(text)`, `base(name)`, `chainRoot(name)`, `sameChain(a, b)`, `REASON_TEXT`, `SWITCHABLE`.++++```js++const { outcome } = require('./engine/chain');++++outcome([++ { key: 'cc:a', runner: 'cc', name: 'a', runnerName: 'Command Code', status: 'failed', updated: 1, error: 'usage limit reached' },++ { key: 'mm:a-r2', runner: 'mm', name: 'a-r2', runnerName: 'MiniMax Code', status: 'done', updated: 2 },++], { cfg, health, counts, models, runners: ['cc', 'mm'] });++// { state: 'done', reason: 'limit', attempt: 1,++// step: { action: 'done', why: 'the task finished' }, … }++```++++`step.action` is one of `wait`, `done`, `switch`, `continue`, `restart`, `wrap-up`, `stop`, `failed`, `none`.++++### `budget.js` — how far one task may go++++Three limits: steps (tool calls), minutes (wall clock) and dollars. A limit that is not set is no limit — an unknown++price is never a zero budget. The first limit reached wins. Steps and minutes buy **one** wrap-up turn, so the work++already done lands; dollars stay a hard stop.++++Public: `limitsFor(cfg, runner, override)`, `evaluate(used, limits)`, `state(status)`, `wrapUp(hit, opts)`,++`wrapUpPrompt(hit)`, `STATES`.++++```js++const { evaluate, wrapUp } = require('./engine/budget');++++evaluate({ steps: 120, minutes: 30, usd: 1.2 }, { steps: 100, minutes: 60, usd: 5 });++// { hit: 'steps', limits: { steps: 100, minutes: 60, usd: 5 }, used: { steps: 120, minutes: 30, usd: 1.2 } }++++wrapUp('steps'); // { action: 'wrap-up', prompt: 'You reached your steps limit…', why: … }++wrapUp('usd'); // { action: 'stop', prompt: null, why: 'a dollar limit is a hard stop: no wrap-up turn' }++```++++### `transcript.js` — one event shape for every harness++++Claude Code stream-json, Codex, Command Code, OpenCode, `dsh --json` and MiniMax Code stream-json all become++`{ t, text, name, input, output, at, … }`. The whole shape is described in [`transcript.schema.json`](transcript.schema.json).++Parsing a file is incremental: only the bytes added since the last read are parsed, so polling a running task every++second stays cheap.++++Public: `parse(file)`, `parseText(text)`, `view(state, from, limit)`, `stats(file)`, `statsOf(state)`,++`persistStats(file)`, `summary(name, input)`, `kindOf(name)`, `clip(value, max)`, `textOf(content)`, `atOf(ts)`.++++```js++const { parseText } = require('./engine/transcript');++++const state = parseText([++ JSON.stringify({ type: 'assistant', message: { model: 'claude-sonnet-5-5', content: [{ type: 'text', text: 'On it.' }] } }),++ JSON.stringify({ type: 'assistant', message: { content: [{ type: 'tool_use', id: 't1', name: 'Bash', input: { command: 'npm test' } }] } }),++].join('\n'));++++state.events.map((e) => e.t); // ['text', 'tool']++state.events[1].name; // 'Bash'++state.events[1].input.text; // '{ "command": "npm test" }' (the input as text)++```++++### `effort.js` — the reasoning level a harness accepts, and the models a provider refuses++++Each harness accepts a different set of levels, so `fitEffort` rounds a wanted level to the nearest one the target++harness takes (a tie goes up) — otherwise a retry hands a runner a level it refuses and it dies before its first step.++The blocked-model list is per harness: a refused model is replaced by one the harness still lists, and a launch with++nothing left is an error, never a silent run with a refused model.++++Public: `fitEffort(runner, effort)`, `stepUp(effort, list)`, `EFFORTS`, `ALIASES`, `ORDER`, `harnessOf(id)`,++`blockedList(blockedModels, runner)`, `isBlocked(blockedModels, runner, model)`, `pickModel({ runner, model, models, blockedModels })`,++`blockModel(list, model, error)`, `unblockModel(list, model)`.++++```js++const { fitEffort, pickModel } = require('./engine/effort');++++fitEffort('cc', 'medium'); // 'high' — Command Code does not take 'medium'++fitEffort('oc', 'medium'); // 'medium' — OpenCode has no effort setting++++pickModel({ runner: 'cx', model: 'gpt-6.1-sol', models: { cx: ['gpt-6.1-sol', 'gpt-6-sol'] },++ blockedModels: { cx: ['gpt-6.1-sol'] } });++// 'gpt-6-sol'++```++++Codes' harness names (`claude-code`, `codex`, `command-code`, `opencode`, `deepseek`) are accepted anywhere a portal++runner id is.++++### `scope.js` — one systemd scope per job++++An agent that starts a server in the background leaves it behind when its shell exits: the server is reparented to the++user's systemd, out of the job's process group, and nothing ever sees it again. Fifteen such servers and headless++browsers ran this workstation out of memory on 2026-10-30 01:38 UTC — the kernel killed the IDE, the portal and every++running job. A scope is a cgroup: everything the job starts stays in it, however it detaches, and `reap()` stops the++scope when the job itself is gone. `ovjobs.slice` caps all jobs together (15G/18G) and each scope caps one job++(6G/8G, `OOMPolicy=continue`).++++Linux with a systemd user manager only: everywhere else `wrap()` returns the command unchanged and `reap()` does++nothing.++++Public: `available()`, `wrap(cmd, args, label)`, `remember(unit, pid, label)`, `reap({ log })`, `stopFor(pid)`,++`SLICE`, `MAP`.++++```js++const scope = require('./engine/scope');++++const { cmd, args, unit } = scope.wrap('node', ['server.js'], 'cc-my-job');++// on Linux with systemd: cmd = 'systemd-run', unit = 'ovjob-cc-my-job-….scope'++// elsewhere: cmd = 'node', unit = null++++await scope.reap({ log: (line) => console.log(line) });++```++++### `runerror.js` — what a failed run really said++++A harness that ends without an answer often leaves the wrapper's own no-result message behind; that message lists++possible causes, not an observed failure, so it is never reported as one. Only a real diagnostic in the feed or on++stderr is, and never a fragment of the launch command (the brief is echoed after a killed child).++++Public: `failureError(feed, stderr, summary)`, `message(value)`, `NO_RESULT`.++++```js++const { failureError } = require('./engine/runerror');++++failureError('{"type":"result","is_error":true,"result":"API Error: usage limit reached"}', '');++// 'API Error: usage limit reached'++```++++## Tests++++`test/engine-*.test.js` in the Codes repo, run by the repo's own runner (`npm test`).nnew file mode 100644@@@ -0,0 +1,102 @@++'use strict';++++/**++ * The per-task budget: how far one task may go before it is ended cleanly.++ *++ * Three limits, all optional: steps (tool calls), minutes (wall clock since the task started) and dollars (what the++ * task's own feed reported). A limit that is not set is no limit: an unknown price means no USD limit, never a zero++ * budget. The defaults come from the config, the runner may have its own, and a single task may override them.++ *++ * When a limit is reached the task stops politely and says so ("budget-stopped"), it is not a failure. Steps and++ * minutes buy one wrap-up turn, so the work already done lands (a commit, a push, a report) instead of sitting in a++ * worktree. Dollars stay a hard stop: the owner's money does not get a farewell turn.++ *++ * Everything here is pure: the caller passes the numbers it already has.++ */++++/** The states a task goes through. */++const STATES = ['queued', 'running', 'done', 'failed', 'budget-stopped'];++++/** The limits a task runs under: the config's defaults, then the runner's own, then the task's override. */++function limitsFor(cfg, runner, override) {++ const b = (cfg && cfg.budget) || {};++ const per = (b.perRunner && b.perRunner[runner]) || {};++ const num = (v) => (Number.isFinite(Number(v)) && Number(v) > 0 ? Number(v) : null);++ const out = {++ steps: num(per.steps != null ? per.steps : b.steps),++ minutes: num(per.minutes != null ? per.minutes : b.minutes),++ usd: num(per.usd != null ? per.usd : b.usd),++ };++ if (override && typeof override === 'object') {++ for (const k of ['steps', 'minutes', 'usd']) if (override[k] != null) out[k] = num(override[k]);++ }++ return out;++}++++/**++ * Compare what a task spent with its limits. The first limit reached wins (steps, then minutes, then dollars).++ *++ * @param {object} used what it spent: { steps, minutes, usd }++ * @param {object} limits what it may spend: { steps, minutes, usd }++ * @returns {{hit: string|null, limits: object, used: object}} which limit was reached, if any++ */++function evaluate(used = {}, limits = {}) {++ const spent = {++ steps: Number(used.steps || 0),++ minutes: used.minutes == null ? null : Number(used.minutes),++ usd: used.usd == null ? null : Number(used.usd),++ };++ const cap = {++ steps: limits.steps == null ? null : Number(limits.steps),++ minutes: limits.minutes == null ? null : Number(limits.minutes),++ usd: limits.usd == null ? null : Number(limits.usd),++ };++ let hit = null;++ if (cap.steps != null && spent.steps >= cap.steps) hit = 'steps';++ else if (cap.minutes != null && spent.minutes != null && spent.minutes >= cap.minutes) hit = 'minutes';++ else if (cap.usd != null && spent.usd != null && spent.usd >= cap.usd) hit = 'usd';++ return {++ hit,++ limits: cap,++ used: { steps: spent.steps, minutes: spent.minutes == null ? null : Math.round(spent.minutes * 10) / 10, usd: spent.usd },++ };++}++++/** The state of a task from its status: a task that stopped on a limit is 'budget-stopped', never a vague 'failed'. */++function state(status) {++ if (status === 'running' || status === 'queued') return status;++ if (status === 'budget-stopped') return 'budget-stopped';++ if (status === 'failed' || status === 'interrupted') return 'failed';++ return 'done';++}++++/**++ * The one turn a task gets after its steps or minutes ran out: land the work, do not start anything new.++ *++ * @param {string} hit which limit was reached++ * @returns {string} the prompt of the wrap-up turn++ */++function wrapUpPrompt(hit) {++ return `You reached your ${hit} limit, so this is one short last turn to land the work. Do not start anything new.++1. Run the tests your change touches. Commit what passes and push it. Open the PR if there is none.++2. If something is half done, leave it out of the commit and say exactly what is left.++3. Stop any local server or browser you started.++4. End with a report: what landed (PRs), what is left, and anything the orchestrator must decide.`;++}++++/**++ * The wrap-up rule: one turn after a steps or minutes stop, never after a dollar stop.++ *++ * @param {string} hit which limit was reached ('steps', 'minutes' or 'usd')++ * @param {object} [opts]++ * @param {boolean} [opts.done] the wrap-up turn already ran++ * @returns {{action: 'wrap-up'|'stop', prompt: string|null, why: string}} what to do now++ */++function wrapUp(hit, opts = {}) {++ if (hit === 'usd') return { action: 'stop', prompt: null, why: 'a dollar limit is a hard stop: no wrap-up turn' };++ if (opts.done) return { action: 'stop', prompt: null, why: 'the wrap-up turn already ran' };++ if (!hit) return { action: 'stop', prompt: null, why: 'no limit was reached' };++ return { action: 'wrap-up', prompt: wrapUpPrompt(hit), why: `one last turn to land the work after the ${hit} limit` };++}++++module.exports = { STATES, limitsFor, evaluate, state, wrapUp, wrapUpPrompt };nnew file mode 100644@@@ -0,0 +1,484 @@++'use strict';++++/**++ * The chain of one task: what each attempt did, why it stopped, and what happens next.++ *++ * Everything here is pure. It takes the attempts (already read by the caller) and returns the outcome and the next++ * step; it never reads a log folder, a marker file or a process table. The portal's own folder stays its business, the++ * hosted runner can use the same code with its own store, and a test needs no disk at all.++ *++ * An attempt is:++ *++ * key its own id, unique ("cc:a", "mm:a-r2")++ * runner the harness that ran it ("cc", "mm", ...)++ * name its name in the chain ("a", "a-r2", "↻ a-r2")++ * runnerName the harness' own name, for the words a person reads++ * status queued | running | done | failed | stopped | interrupted | budget-stopped++ * updated when it last changed (ms)++ * error what it said when it failed++ * next the key of the attempt this one spawned (a retry)++ * prev the key of the attempt before it++ * parent the key of the attempt that carried this one on++ * resolvedNote the owner's note ("marked as handled")++ * stopped true when it was stopped on purpose++ * budget { hit } — which limit it reached++ * wrapUp true when its wrap-up turn already ran++ * facts { exit, finalText, hasFinal, apiError, stopKind, session }++ *++ * The failure reasons are the portal's: limit (the plan ran out), auth (signed out), capacity (the model was busy),++ * refused (turned off), error (the work itself went wrong), budget (a limit of the owner's), stopped (someone ended++ * it on purpose) and crash (the process died without an answer).++ */++++const { NO_RESULT } = require('./runerror');++const { stepUp, EFFORTS } = require('./effort');++const budget = require('./budget');++++/** The runners the portal's chains are made of. */++const RUNNERS = /^(dsh|cx|cc|oc|pg|mm|cm)$/;++++/** The reasons the runner switch owns: a limit, a lost sign-in, a busy model, a runner turned off. */++const SWITCHABLE = new Set(['limit', 'auth', 'capacity', 'refused']);++++/** The reasons a job stopped, in words. */++const REASON_TEXT = {++ stopped: 'was cancelled before it finished',++ limit: 'hit its usage limit',++ auth: 'lost its sign-in',++ capacity: 'model was at capacity',++ refused: 'was turned off in the portal',++ error: 'stopped with an error',++ budget: 'stopped at its limit',++ unknown: 'stopped',++ crash: 'the process died without an answer',++ timeout: 'it ran out of time',++ incomplete: 'it stopped before the work was done',++ context: 'the context grew too big',++ hung: 'it went quiet for too long',++};++++// A job that stopped for a reason the engine can fix by itself: the context is too big, the clock ran out, the agent++// said it was not done, the process died without an answer, or the log went quiet.++const CONTEXT_RE = /prompt is too long|context length|maximum context|context window|too many tokens|input too long|context_length_exceeded|exceeds the context|prompt exceeds/i;++const INCOMPLETE_RE = /i'?ll continue|ran out of|max(?:imum)? turns?|not finished|remaining steps|still (?:need|needs|to do|to finish)|more (?:work )?(?:to do|remains)|to be continued|incomplete/i;++++/**++ * Why a run failed, from what it said.++ *++ * @param {string} text the error++ * @returns {string} limit | auth | capacity | refused | error | unknown++ */++function reason(text) {++ // Remove only the wrapper's speculative causes; retain any real diagnostic appended to it.++ const s = String(text || '').replace(NO_RESULT, 'Command Code ended without a result');++ if (!s) return 'unknown';++ if (/turned off in the agent portal|portal limit/i.test(s)) return 'refused';++ if (/usage limit|reached your [\w -]*limit|limit (exceeded|reached)|rate.?limit|quota|insufficient[ _]balance|insufficient_quota|plan_allowance_exhausted|out of credits|\b402\b|(?:HTTP(?: status)?|status(?: code)?|Request rejected)\s*[:=(]?\s*429\b/i.test(s)) return 'limit';++ if (/authenticat|log ?in again|run .?cmd login|\b401\b|api key (has )?expired|invalid api key|unauthori[sz]ed|not signed in/i.test(s)) return 'auth';++ if (/at capacity|overloaded|\b529\b|\b503\b|temporarily unavailable|server is busy/i.test(s)) return 'capacity';++ if (/mcode exec failed:.*Internal server error.*\(50[024]\)/i.test(s) || /(?:HTTP(?: status)?|status(?: code)?)\s*[:=]?\s*50[024]\b/i.test(s)) return 'capacity';++ return 'error';++}++++/**++ * The job a name belongs to: "bench-7-finish", "bench7", "bench7-r2" and "bench7-c1" are one job.++ *++ * @param {string} name the attempt's name++ * @returns {string} the job's name++ */++function base(name) {++ let n = String(name || '').toLowerCase();++ for (let i = 0; i < 3; i++) n = n.replace(/[-_.](r\d+|c\d+|retry\d*|resume\d*|again|continued?|finish(ed)?|fix(es)?|followup|take\d+)$/, '');++ n = n.replace(/(\d)f$/, '$1');++ return n.replace(/[^a-z0-9]/g, '');++}++++/**++ * The chain a job belongs to: the name without its attempt number ("x-r2", "x-c1" → "x"). The fence of a chain covers++ * every attempt of it.++ *++ * @param {string} name the attempt's name++ * @returns {string} the chain's root++ */++function chainRoot(name) {++ let n = String(name || '');++ for (let i = 0; i < 4; i++) {++ const m = n.match(/-(r\d+|c\d+)$/i);++ if (!m) break;++ n = n.slice(0, -m[0].length);++ }++ return n;++}++++/**++ * Two names are one chain when one is the other plus a suffix: "x" and "x-b" (a renamed successor) are one job,++ * "seo-batch-a" and "seo-batch-b" are not.++ *++ * @param {string} a one name++ * @param {string} b the other++ * @returns {boolean} true when they are one chain++ */++function sameChain(a, b) {++ const x = chainRoot(a).toLowerCase(), y = chainRoot(b).toLowerCase();++ if (!x || !y) return false;++ return x === y || x.startsWith(y + '-') || y.startsWith(x + '-');++}++++/** The attempt that carried each attempt on (the latest one wins). */++function carriedMap(runs, byKey) {++ const carried = new Map();++ for (const r of runs) if (r.parent && byKey.has(r.parent) && (!carried.has(r.parent) || carried.get(r.parent).updated < r.updated)) carried.set(r.parent, r);++ return carried;++}++++/** The end of a chain: the run that ended it, and the runs it passed through. */++function settleChain(runs, byKey, carried, start) {++ let end = start, hops = 0;++ const trail = [start];++ while (hops++ < 12) {++ const c = carried.get(end.key);++ const n = end.next;++ const by = end.resolved && end.resolved.by;++ const step = c && c !== end ? c : n && byKey.has(n) && n !== end.key ? byKey.get(n) : by && byKey.has(by) && by !== end.key && by !== start.key ? byKey.get(by) : null;++ if (!step || trail.includes(step)) break;++ end = step;++ trail.push(end);++ }++ return { end, trail };++}++++/**++ * The attempt that ended a chain, and the attempts it passed through.++ *++ * @param {object[]} attempts the attempts of the chain++ * @param {string|object} [start] the attempt to start from (the latest by default)++ * @returns {object|null} the attempt that ended the chain++ */++function settle(attempts, start) {++ const runs = annotate(attempts);++ if (!runs.length) return null;++ const from = (start && (start.key || start)) ? runs.find((r) => r.key === (start.key || start)) : null;++ const latest = runs.reduce((a, b) => (b.updated > a.updated ? b : a));++ const byKey = new Map(runs.map((r) => [r.key, r]));++ return settleChain(runs, byKey, carriedMap(runs, byKey), from || latest).end;++}++++/**++ * The attempts of a chain, with the reason of each failure, what resolved it and the attempt number.++ *++ * @param {object[]} attempts the attempts++ * @returns {object[]} new attempts (the input is never changed)++ */++function annotate(attempts) {++ const runs = (attempts || []).map((a) => ({ ...a }));++ const byKey = new Map(runs.map((r) => [r.key, r]));++ const jobs = runs.filter((r) => RUNNERS.test(r.runner));++ const groups = new Map();++ for (const r of jobs) {++ const b = base(r.name);++ if (!groups.has(b)) groups.set(b, []);++ groups.get(b).push(r);++ }++ // "crawl-candidates2" belongs with "crawl-candidates" when that job exists.++ for (const [b, list] of [...groups]) {++ const m = b.match(/^(.*[a-z])\d$/);++ if (m && groups.has(m[1])) { groups.get(m[1]).push(...list); groups.delete(b); }++ }++ // The chain of every job: the attempts of it ("x-r2", "x-c1") and its renamed successors ("x-b"). A renamed++ // successor belongs to the chain of the job it continues, even when the links between them are gone.++ const chains = new Map();++ for (const r of jobs) {++ const k = chainRoot(r.name).toLowerCase();++ if (!chains.has(k)) chains.set(k, []);++ chains.get(k).push(r);++ }++ const kinOf = (r) => {++ const k = chainRoot(r.name).toLowerCase(), out = [];++ for (const [root, list] of chains) if (root === k || root.startsWith(k + '-') || k.startsWith(root + '-')) out.push(...list);++ return out;++ };++ for (const r of jobs) {++ if (r.status === 'failed' || r.status === 'stopped' || r.status === 'interrupted') r.reason = r.status === 'failed' ? reason(r.error) : r.status === 'interrupted' ? 'crash' : 'stopped';++ if (r.status !== 'failed' && r.status !== 'stopped' && r.status !== 'interrupted') continue;++ // The owner stopped it on purpose (the portal, the budget or a human): it is not a failure to attend to.++ if (r.stopped) { r.resolved = { how: 'stopped', note: 'stopped on purpose' }; continue; }++ if (r.resolvedNote) { r.resolved = { how: 'note', note: r.resolvedNote }; continue; }++ // Follow explicit retry links to the end of the chain.++ let next = r.next, hops = 0, end = null;++ while (next && byKey.has(next) && hops++ < 8) { end = byKey.get(next); next = end.next; }++ if (!end) {++ const group = groups.get(base(r.name)) || [];++ const seen = new Set(group.map((s) => s.key));++ const kin = kinOf(r).filter((s) => !seen.has(s.key));++ const later = [...group, ...kin].filter((s) => s.key !== r.key && s.updated >= r.updated - 60000).sort((a, b) => b.updated - a.updated);++ end = later.find((s) => s.status === 'running' || s.status === 'queued') || later.find((s) => s.status === 'done') || later[0] || null;++ }++ if (end) {++ const how = end.status === 'done' ? 'done' : end.status === 'running' || end.status === 'queued' ? 'running' : 'failed';++ r.resolved = { how, by: end.key, byRunner: end.runnerName, note: how === 'done' ? `superseded by ${end.key}` : how === 'running' ? `taken over by ${end.key}` : `failed again as ${end.key}` };++ // The list says "finished by <name>" when there is one; the note keeps the key of the successor.++ if (how === 'done') r.resolved.byName = end.name;++ }++ }++ // Where each chain really ends (2026-10-29, the owner: a "✓ failed again as …" line, a run a later session++ // finished still called failed, a step-limit stop called a failure, and a page that said something else). A++ // successor is followed through its own retry link, through a session that carried it on (a portal "↻" continuation++ // names it as its parent) and through what resolved it, to the run that ended the chain. A step-limit stop with++ // nothing after it is unfinished work: it needs attention, like a failure.++ const carried = carriedMap(runs, byKey);++ // How a run in the path ended, in the words a person reads.++ const said = (x) => ({ done: 'finished', running: 'running', queued: 'queued', 'budget-stopped': 'stopped at its step limit', failed: 'failed', stopped: 'stopped', interrupted: 'crashed' })[x.status] || x.status;++ const trails = new WeakMap(); // the runs a chain passed through, kept off the runs (the list is sent as JSON)++ const settleFrom = (start) => {++ const t = settleChain(runs, byKey, carried, start);++ trails.set(t.end, t.trail);++ return t.end;++ };++ const ended = (r, end) => {++ const name = end.name;++ if (end.status === 'done') return { how: 'done', by: end.key, byName: name, byRunner: end.runnerName, note: `superseded by ${end.key}` };++ if (end.status === 'running' || end.status === 'queued') return { how: 'running', by: end.key, byName: name, byRunner: end.runnerName, note: `taken over by ${end.key}` };++ if (end.resolved && (end.resolved.how === 'note' || end.resolved.how === 'stopped')) return { how: 'note', by: end.key, byName: name, byRunner: end.runnerName, note: `carried on as ${name}: ${end.resolved.note}` };++ if (end.status === 'budget-stopped') return { how: 'budget', by: end.key, byName: name, byRunner: end.runnerName, note: `carried on as ${name}, which stopped at its step limit before it finished` };++ return { how: 'failed', by: end.key, byName: name, byRunner: end.runnerName, note: `tried again as ${name}, which failed too` };++ };++ const ending = (r, end) => ({ ...ended(r, end), path: [r, ...(trails.get(end) || [end]).filter((x) => x !== r)].map((x) => ({ key: x.key, name: x.name, state: said(x) })) });++ // The step-limit stops first (a handled note, then what carried them on), so a chain that ends in one reads it.++ const stops = runs.filter((r) => r.status === 'budget-stopped');++ for (const r of stops) {++ r.reason = 'budget';++ if (r.resolvedNote) r.resolved = { how: 'note', note: r.resolvedNote };++ }++ for (const r of stops) {++ if (r.resolved) continue;++ const end = settleFrom(r);++ if (end !== r) r.resolved = ending(r, end);++ }++ for (const r of runs) {++ if (r.status === 'budget-stopped' || !r.resolved || !r.resolved.by || !byKey.has(r.resolved.by)) continue;++ r.resolved = ending(r, settleFrom(byKey.get(r.resolved.by)));++ }++ // A handled note that names the job the work went to ("being integrated by mm:x") is followed to where that job++ // ended, so the line says whether the work was done, not only that someone wrote a note.++ const named = (note) => (String(note || '').match(/\b(?:dsh|cx|cc|oc|pg|mm|cm|cd):[A-Za-z0-9._-]*[A-Za-z0-9]/g) || []).find((k) => byKey.has(k));++ for (const r of runs) {++ const res = r.resolved;++ if (!res || res.how !== 'note') continue;++ const k = res.by ? named(byKey.get(res.by) && byKey.get(res.by).resolved && byKey.get(res.by).resolved.note) || named(res.note) : named(res.note);++ if (!k || k === r.key) continue;++ const end = settleFrom(byKey.get(k));++ if (end.status !== 'done' && end.status !== 'running' && end.status !== 'queued') continue;++ r.resolved = { ...ending(r, end), said: res.note };++ // The run whose note named it stays in the path ("a → a-r2 (stopped at its step limit) → integrate …").++ const via = res.by && byKey.get(res.by);++ if (via && !r.resolved.path.some((h) => h.key === via.key)) r.resolved.path.splice(1, 0, { key: via.key, name: via.name, state: said(via) });++ }++ // Attempt numbers along each chain (for "attempt 3").++ for (const r of jobs) {++ let n = 1, p = r.prev, hops = 0;++ while (p && byKey.has(p) && hops++ < 8) { n++; p = byKey.get(p).prev; }++ if (n > 1) r.attempt = n;++ }++ return runs;++}++++/**++ * The next runner (and model) for a failed job, or null when nothing fits right now.++ *++ * @param {object} run the failed attempt++ * @param {object} ctx++ * @param {object} ctx.cfg the config (subagents per runner, fallback.order)++ * @param {object} ctx.health the health of each runner++ * @param {object} ctx.counts how many jobs each runner runs now++ * @param {object} ctx.models the catalogs of each runner++ * @param {string[]} ctx.runners every runner++ * @param {string} [ctx.tier] the tier of the job++ * @returns {{runner: string, model: string|null, why: string}|null} the next runner++ */++function nextRunner(run, { cfg, health, counts, models, runners, tier } = {}) {++ const why = run.reason;++ // A stop is not a failure: someone ended that run on purpose (a kill, a duplicate taken over), so a second runner++ // would only redo the work (2026-10-29: a stopped cc pin job was queued again on cm, and the mm duplicate with it).++ if (why === 'stopped') return null;++ // Phoenix Grove's Taster allowance is tiny: it takes only light jobs.++ const usable = (k) => (k !== 'pg' || tier === 'basic') && runners.includes(k) && cfg.subagents[k] && cfg.subagents[k].enabled && (counts[k] || 0) < cfg.subagents[k].max++ && health[k] && (health[k].state === 'ok');++ // A real error: a second opinion — the next runner in the order (not the same one), with the error in the brief.++ if (why === 'error') {++ const order = (cfg.fallback.order || []).filter((k) => runners.includes(k) && k !== run.runner);++ const k = order.find((x) => usable(x));++ return k ? { runner: k, model: cfg.subagents[k].model || null, why: `${run.runnerName} stopped with an error; a second try on another runner` } : null;++ }++ if (why === 'capacity' && models[run.runner] && cfg.subagents[run.runner] && cfg.subagents[run.runner].enabled) {++ const list = models[run.runner];++ const cur = run.model && list.indexOf(run.model);++ const other = list.find((m, i) => i > (cur >= 0 ? cur : 0) && m !== run.model);++ if (other) return { runner: run.runner, model: other, why: `${run.model || 'its model'} was at capacity` };++ }++ const order = (cfg.fallback.order || []).filter((k) => runners.includes(k));++ const start = order.indexOf(run.runner);++ const rotated = start >= 0 ? order.slice(start + 1).concat(order.slice(0, start)) : order;++ for (const k of rotated) {++ if (k === run.runner) continue;++ if (usable(k)) return { runner: k, model: cfg.subagents[k].model || null, why: `${run.runnerName} ${REASON_TEXT[why] || 'stopped'}` };++ }++ return null;++}++++/**++ * The note a continued attempt gets: what the earlier attempt did and where it stopped.++ *++ * @param {object} run the earlier attempt++ * @param {string} logFile where its log is++ * @returns {string} the note++ */++function continuationNote(run, logFile) {++ return `\n\n---\n## Continuation (written by the agent portal)\n\nAn earlier attempt at this job ran on **${run.runnerName}** and ${REASON_TEXT[run.reason] || 'stopped'}${run.error ? `: "${String(run.error).slice(0, 200)}"` : ''}. Its log is ${logFile}.\nBefore anything else, look at the folder's current state (\`git status\`, \`git diff\`, \`git log -3\`) and the end of that log: keep the work it finished, do not redo it, and continue from where it stopped. If the job is already complete, check it and report that.\n`;++}++++/**++ * The facts of an attempt, from what its transcript holds.++ *++ * @param {object} attempt the attempt++ * @param {object[]} [events] the transcript's events++ * @returns {object} { exit, finalText, hasFinal, apiError, stopKind, session }++ */++function facts(attempt = {}, events = null) {++ const f = { ...(attempt.facts || {}) };++ const list = events || f.events || [];++ let finalText = f.finalText || '', hasFinal = f.hasFinal || false, apiError = f.apiError || '';++ for (const e of list) {++ if (e.t === 'final') { hasFinal = true; if (e.text) finalText = e.text; if (e.isError && e.text && !apiError) apiError = e.text; }++ else if (e.t === 'error' && !apiError) apiError = String(e.text || '');++ }++ if (!apiError && attempt.error) apiError = String(attempt.error);++ return {++ exit: f.exit == null ? null : f.exit,++ finalText,++ hasFinal,++ apiError,++ stopKind: f.stopKind || '',++ session: f.session || null,++ usage: f.usage || null,++ };++}++++/**++ * Why an attempt stopped, or null when nothing is to be done about it.++ *++ * The order matters: a context error explains the stop even when there is an API error on top of it, and a limit, a++ * lost sign-in or a busy model belong to the runner switch, not to a continue on the same runner.++ *++ * @param {object} attempt the attempt, with its facts++ * @returns {{reason: string, facts: object}|null} why it stopped++ */++function detect(attempt = {}) {++ // An attempt that finished is never continued.++ if (attempt.status === 'done') return null;++ const f = facts(attempt);++ if (CONTEXT_RE.test(f.apiError || '') || CONTEXT_RE.test(f.finalText || '')) return { reason: 'context', facts: f };++ // An attempt that was interrupted (killed, or a service restart) is a crash: its session can be resumed.++ if (attempt.status === 'interrupted') return { reason: 'crash', facts: f };++ const why = f.apiError ? reason(f.apiError) : 'error';++ if (SWITCHABLE.has(why)) return null;++ if (f.exit === 124) return { reason: 'timeout', facts: f };++ if (f.exit === 8) return { reason: 'incomplete', facts: f };++ if (/max_?tokens|length/i.test(f.stopKind || '')) return { reason: 'incomplete', facts: f };++ if (f.hasFinal && INCOMPLETE_RE.test(f.finalText)) return { reason: 'incomplete', facts: f };++ if (!f.hasFinal && !f.apiError) return { reason: 'crash', facts: f };++ return null;++}++++/**++ * The next step of a chain: what should happen to the task now.++ *++ * @param {object[]} attempts the attempts (annotated or not)++ * @param {object} [opts] what the caller knows: { cfg, health, counts, models, runners, tier }++ * @returns {object} { action, why, … } — action is one of wait, done, switch, continue, restart, wrap-up, stop, failed++ */++function nextStep(attempts, opts = {}) {++ const runs = annotate(attempts);++ if (!runs.length) return { action: 'none', why: 'there are no attempts' };++ const end = chainEnd(runs);++ return step(runs, end, opts);++}++++/** The end of a chain: its latest attempt, followed to whatever ended it. */++function chainEnd(runs) {++ if (!runs.length) return null;++ const latest = runs.reduce((a, b) => (b.updated > a.updated ? b : a));++ const byKey = new Map(runs.map((r) => [r.key, r]));++ return settleChain(runs, byKey, carriedMap(runs, byKey), latest).end;++}++++/** The next step, given the chain's end. */++function step(runs, end, opts = {}) {++ if (!end) return { action: 'none', why: 'there are no attempts' };++ if (end.status === 'running' || end.status === 'queued') return { action: 'wait', why: 'the task is still running' };++ if (end.status === 'done') return { action: 'done', why: 'the task finished' };++ const why = end.reason || reason(end.error);++ if (why === 'stopped') return { action: 'stop', why: 'the task was stopped on purpose' };++ if (why === 'budget') {++ const hit = (end.budget && end.budget.hit) || 'steps';++ const wrap = budget.wrapUp(hit, { done: !!end.wrapUp });++ return { action: wrap.action, hit, prompt: wrap.prompt, why: wrap.why };++ }++ // The same session continues the task when it stopped early (crash, timeout, incomplete, hung); a context that++ // grew too big starts fresh with a compacted brief instead. This comes before the runner switch: a context that++ // overflowed is not a worker's fault, and another runner would hit the same wall.++ const hit = end.hung ? { reason: 'hung', facts: facts(end) } : detect(end);++ if (hit) {++ if (hit.reason === 'context') return { action: 'restart', why: 'the context grew too big: a fresh session with a compacted brief' };++ const session = hit.facts.session;++ if (!session) return { action: 'failed', why: `the task stopped (${hit.reason}) and there is no session to continue` };++ return { action: 'continue', session, why: `the task stopped (${hit.reason}): continue it in the same session` };++ }++ // The runner switch: a limit, a lost sign-in, a busy model, a runner turned off, or a real error (a second++ // opinion). A job retried three times is left to the owner.++ if (SWITCHABLE.has(why) || why === 'error') {++ const tries = end.attempt || 1;++ const max = why === 'error' ? 2 : 4;++ if (tries >= max) return { action: 'failed', why: `the task was already retried ${tries} times; the owner must decide` };++ const choice = nextRunner(end, opts);++ if (!choice) return { action: 'failed', why: `no other runner can take it now (${why})` };++ const out = { action: 'switch', runner: choice.runner, model: choice.model, why: choice.why };++ // A real error is retried one reasoning level up: the same attempt again rarely helps.++ if (why === 'error') {++ const up = stepUp(end.effort, EFFORTS[choice.runner]);++ if (up) out.effort = up;++ }++ return out;++ }++ return { action: 'failed', why: 'the task failed' };++}++++/**++ * The outcome of a task and the next step: the whole chain, in one call.++ *++ * @param {object[]} attempts the attempts of the task++ * @param {object} [opts] what the caller knows: { cfg, health, counts, models, runners, tier }++ * @returns {{state: string, reason: string|null, resolved: object|null, attempt: number, path: object[], end: object, step: object, attempts: object[]}}++ */++function outcome(attempts, opts = {}) {++ const runs = annotate(attempts);++ const end = chainEnd(runs);++ const state = !end ? 'none' : end.status === 'done' ? 'done' : end.status === 'budget-stopped' ? 'budget-stopped'++ : end.status === 'failed' || end.status === 'interrupted' ? 'failed' : end.status;++ return {++ state,++ reason: end ? (end.reason || null) : null,++ resolved: end ? (end.resolved || null) : null,++ attempt: end ? (end.attempt || 1) : 0,++ path: (end && end.resolved && end.resolved.path) || [],++ end: end ? { key: end.key, runner: end.runner, name: end.name, status: end.status } : null,++ step: step(runs, end, opts),++ attempts: runs,++ };++}++++module.exports = {++ RUNNERS, SWITCHABLE, REASON_TEXT,++ reason, base, chainRoot, sameChain,++ annotate, settle, nextRunner, continuationNote,++ facts, detect, nextStep, outcome,++};nnew file mode 100644@@@ -0,0 +1,148 @@++'use strict';++++/**++ * Reasoning effort and blocked models: the two places where a runner is handed something it will refuse.++ *++ * Effort: every harness accepts a different set of levels. The 2026-10-29 incident: a job retried on another runner++ * copied Codex's "medium" to Command Code, which died before its first step ("Unknown effort \"medium\". Supported: off,++ * low, high, max."). `fitEffort` rounds a wanted level to the nearest one the target harness accepts (a tie goes up), so++ * no caller can hand a runner a level it refuses. The levels are uneven ("off" and "ultra" exist on one harness only),++ * so the ordering must know all of them or a cross-runner value passes through untouched.++ *++ * Blocked models: a provider can refuse a model on one account ("not supported with a ChatGPT account"). The list is++ * per harness; a blocked model is replaced by another one the harness still lists, and a launch with no unblocked model++ * left is an error, never a silent run with a refused model.++ *++ * The keys are the portal's short runner ids, so the portal's own ladders and catalogs keep working. Codes calls the++ * same harnesses by their full names; `harnessOf` maps those onto the short ids.++ */++++/** The reasoning levels each harness accepts (empty: the harness has no setting). */++const EFFORTS = {++ cx: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'],++ cc: ['off', 'low', 'high', 'max'],++ mm: ['low', 'medium', 'high', 'xhigh', 'max'],++ cm: ['low', 'medium', 'high', 'xhigh', 'max'],++ pg: ['low', 'medium', 'high', 'max'],++ cd: ['low', 'high', 'max'],++ claude: ['low', 'medium', 'high', 'xhigh', 'max'],++ dsh: ['none', 'low', 'high', 'max'],++ oc: [],++};++++/** Codes' harness ids and the portal's short runner ids for the same harness. */++const ALIASES = {++ 'claude-code': 'claude',++ codex: 'cx',++ 'command-code': 'cc',++ opencode: 'oc',++ deepseek: 'dsh',++ minimax: 'mm',++ grove: 'pg',++};++++/** Every level, from the weakest to the strongest. */++const ORDER = ['off', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'];++++/** The short runner id of a harness, whoever named it. */++function harnessOf(id) {++ const key = String(id || '').toLowerCase();++ return ALIASES[key] || key;++}++++/**++ * The nearest level the harness accepts for the wanted one (a tie goes up).++ *++ * @param {string} runner the harness++ * @param {string|null} effort the wanted level++ * @returns {string|null} a level the harness accepts, or the wanted one when the harness has no effort setting++ */++function fitEffort(runner, effort) {++ const list = EFFORTS[harnessOf(runner)] || [];++ if (!effort || !list.length || list.includes(effort)) return effort;++ const at = ORDER.indexOf(effort);++ if (at < 0) return effort;++ for (let d = 0; d < ORDER.length; d++) for (const i of [at + d, at - d]) if (list.includes(ORDER[i])) return ORDER[i];++ return effort;++}++++/**++ * One reasoning level up, for a retry of a real failure: the same attempt again rarely helps.++ *++ * @param {string|null} effort the level this attempt used++ * @param {string[]|null} list the levels the runner accepts (null: any)++ * @returns {string|null} the next level++ */++function stepUp(effort, list) {++ const order = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];++ const i = order.indexOf(effort || 'medium');++ for (let j = i + 1; j < order.length; j++) if (!list || list.includes(order[j])) return order[j];++ return effort || null;++}++++/** The blocked models of one harness. */++function blockedList(blockedModels, runner) {++ const list = (blockedModels || {})[harnessOf(runner)];++ return Array.isArray(list) ? list : [];++}++++/** Is this model refused by the provider on this harness's account? */++function isBlocked(blockedModels, runner, model) {++ if (!model) return false;++ return blockedList(blockedModels, runner).includes(model);++}++++/** The catalog of one harness, by either of its names. */++function catalogFor(models, runner) {++ const all = models || {};++ return all[runner] || all[harnessOf(runner)] || [];++}++++/**++ * The model a launch may use: the wanted one, unless the provider refused it, then the first the harness still lists.++ *++ * @param {object} what++ * @param {string} what.runner the harness++ * @param {string|null} what.model the wanted model++ * @param {object} what.models the catalogs, per harness++ * @param {object} what.blockedModels the blocked lists, per harness++ * @returns {string|null} the model to launch with++ * @throws {Error} when every model the harness lists is blocked++ */++function pickModel({ runner, model, models, blockedModels } = {}) {++ const blocked = blockedList(blockedModels, runner);++ if (!model || !blocked.includes(model)) return model;++ const replacement = catalogFor(models, runner).find((m) => !blocked.includes(m));++ if (!replacement) throw new Error(`No unblocked model available for ${runner}`);++ return replacement;++}++++/** The provider messages that mean "this model is not available to you". */++const REFUSAL_RE = /not supported when using|model_not_found|not available on your plan|model_not_in_plan/i;++++/**++ * The blocked list after a refusal: the model is added when the provider said it does not have it, and left alone++ * otherwise (a crash is not a missing model).++ *++ * @param {string[]} list the current blocked list++ * @param {string} model the model that was refused++ * @param {string} error the provider's message++ * @returns {string[]} the new list++ */++function blockModel(list, model, error) {++ if (!model || !REFUSAL_RE.test(String(error || ''))) return Array.isArray(list) ? list : [];++ const current = Array.isArray(list) ? list : [];++ if (current.includes(model)) return current;++ return [...current, model];++}++++/** The blocked list without one model. */++function unblockModel(list, model) {++ return (Array.isArray(list) ? list : []).filter((m) => m !== model);++}++++module.exports = {++ EFFORTS, ALIASES, ORDER, REFUSAL_RE,++ harnessOf, fitEffort, stepUp,++ blockedList, isBlocked, catalogFor, pickModel, blockModel, unblockModel,++};nnew file mode 100644@@@ -0,0 +1,44 @@++'use strict';++++/**++ * The agent engine: the pieces of the private agent portal that Codes, the CLI and the hosted runner all share.++ *++ * engine.router what kind of task is this, and which runner may take it++ * engine.chain what each attempt did, why it stopped, and what happens next (pure)++ * engine.budget how far one task may go, and the wrap-up turn that lands the work++ * engine.transcript one event shape for every harness's log++ * engine.effort the reasoning level a harness accepts, and the models a provider refuses++ * engine.scope one systemd scope per job, so nothing is left behind++ * engine.runerror what a failed run really said++ *++ * The modules do not depend on each other in a cycle, and none of them reads the portal's log folder.++ */++++const router = require('./router');++const chain = require('./chain');++const budget = require('./budget');++const transcript = require('./transcript');++const effort = require('./effort');++const scope = require('./scope');++const runerror = require('./runerror');++++module.exports = {++ router,++ chain,++ budget,++ transcript,++ effort,++ scope,++ runerror,++ // The names a caller usually wants, in one place.++ classify: router.classify,++ pick: router.pick,++ outcome: chain.outcome,++ nextStep: chain.nextStep,++ evaluate: budget.evaluate,++ limitsFor: budget.limitsFor,++ parse: transcript.parse,++ parseText: transcript.parseText,++ fitEffort: effort.fitEffort,++ pickModel: effort.pickModel,++};nnew file mode 100644@@@ -0,0 +1,171 @@++'use strict';++++/**++ * The tier router: what kind of job is this, and which runner may take it.++ *++ * A brief is classified into one of five tiers — basic, research, standard, hard, design — and each tier has its own++ * ladder, cheapest suitable first: free models for basic chores, flat-price plans for everyday code, the strongest++ * models only for hard problems and design. A brief can force its tier with a line "tier: basic|research|standard|hard|++ * design".++ *++ * Only runners that are on, healthy and have a free slot are chosen, and a runner whose recent jobs mostly failed for++ * real is skipped until it does better (the record comes from the caller's run list, never from a file here). Pay-per-++ * token DeepSeek comes after the plans that are already paid for.++ *++ * The ladder below is the portal's, so the portal's jobs keep their routing. Codes supplies its own ladder (its++ * harness ids differ) through `pick(prompt, { ladder })`.++ */++++const fs = require('fs');++const path = require('path');++const { fitEffort, stepUp, EFFORTS } = require('./effort');++++const LADDER = {++ // Chores: renames, docs, formatting, counting, simple checks — free and plan Flash models first.++ basic: [['oc', 'opencode-go/beam-free', null], ['cc', 'reflection/beam:free', 'low'], ['pg', 'deepseek-v4.1-flash-1017', 'low'], ['mm', null, 'low'], ['cx', 'gpt-6-luna', 'low'], ['dsh', 'deepseek-flash', 'none']],++ // Reading, comparing, web research, audits: models with search, middle reasoning.++ research: [['cx', 'gpt-6-sol', 'medium'], ['mm', null, 'medium'], ['dsh', 'deepseek-flash', 'high'], ['cc', 'deepseek/deepseek-v4.1-flash', 'high'], ['oc', 'opencode-go/kimi-k3', null]],++ // Everyday features and fixes: flat-price plans first.++ standard: [['mm', null, 'high'], ['cx', 'gpt-6-sol', 'medium'], ['cc', 'deepseek/deepseek-v4.1-flash', 'high'], ['oc', 'opencode-go/kimi-k2.7-code', null], ['dsh', 'deepseek-flash', 'high']],++ // Root causes, architecture, security, migrations: the strongest models.++ // DeepSeek V4 Pro only as the very last resort (owner, 10-29: it costs a lot more and rarely earns it).++ hard: [['cx', 'gpt-6-astra', 'high'], ['mm', null, 'max'], ['cc', 'deepseek/deepseek-v4.1-flash', 'high'], ['oc', 'opencode-go/kimi-k3.1-pro', null], ['dsh', 'deepseek-flash', 'max'], ['dsh', 'deepseek-v4-pro', 'max']],++ // Layout, styling, UI: strong models with taste (Opus reviews later through OPUS-REVIEW-QUEUE.md).++ // Astra only in the hard tier (owner, 10-29: "does it really need Astra for this?").++ design: [['cx', 'gpt-6-sol', 'high'], ['mm', null, 'high'], ['cc', 'deepseek/deepseek-v4.1-flash', 'high'], ['dsh', 'deepseek-flash', 'high']],++};++++/** The tiers a brief can fall into. */++const TIERS = ['basic', 'research', 'standard', 'hard', 'design'];++++// The brief a launch prompt points at: "read /…/agents/jobs/x.brief.md and follow it" is a pointer, not a task, so++// the job is judged by the brief it names (2026-10-29: a pin job was routed as "a short question" to a free model++// because its prompt was one sentence long). The job's own brief counts too (jobs/<name>.brief.md, jobs/<name>.task.md).++const BRIEF_RE = /(?:^|[\s'"`(])([^\s'"`)]*\/jobs\/[\w.-]+\.(?:md|txt))/g;++++/**++ * The brief a prompt names, read from the jobs folder.++ *++ * @param {string} prompt the launch prompt++ * @param {string} JOBS the jobs folder++ * @param {string} [name] the job's own name++ * @returns {{text: string, file: string}|null} the brief, or null when there is none++ */++function briefFor(prompt, JOBS, name) {++ const text = String(prompt || '');++ const cands = [];++ for (const m of text.matchAll(BRIEF_RE)) {++ const raw = m[1];++ cands.push(path.isAbsolute(raw) ? raw : path.join(JOBS, path.basename(raw)));++ }++ if (name) cands.push(path.join(JOBS, `${name}.brief.md`), path.join(JOBS, `${name}.task.md`));++ for (const p of cands) {++ if (!p) continue;++ let t = null;++ try { t = fs.readFileSync(p, 'utf8'); } catch { continue; }++ if (!t || !t.trim() || t.trim() === text.trim()) continue; // the prompt itself is no brief++ return { text: t, file: p };++ }++ return null;++}++++/** The tier a line forces, if it forces one. */++function forcedTier(text) {++ const m = String(text || '').match(/^\s*tier:\s*(basic|research|standard|hard|design)\b/im);++ return m ? m[1].toLowerCase() : null;++}++++/**++ * The tier of a brief, from the brief itself.++ *++ * @param {string} text the brief (or the prompt)++ * @param {object} [opts]++ * @param {string} [opts.brief] the file the brief came from++ * @param {boolean} [opts.whole] also look for the tier line past the first 4000 characters (a brief may be long)++ * @returns {{tier: string, why: string, brief?: string}} the tier and the words that say so++ */++function classifyText(text, opts = {}) {++ const p = String(text || ''), head = p.slice(0, 4000);++ const said = opts.brief ? { brief: opts.brief } : {};++ const forced = forcedTier(head) || (opts.whole ? forcedTier(p) : null);++ if (forced) return { tier: forced, why: 'the brief says so', ...said };++ if (/\b(race condition|root cause|architecture|security (review|fix|hole)|migration|data loss|deadlock|concurrency bug|memory leak|performance regression|hard bug)\b/i.test(head)) return { tier: 'hard', why: 'a hard problem', ...said };++ if (/\b(design pass|redesign|layout|styling|visual|polish the|ui\b|ux\b|css|look and feel|screenshots? at 390)\b/i.test(head)) return { tier: 'design', why: 'design work', ...said };++ if (/\b(research|investigate|survey|compare|audit|competitor|do not edit any repository|report only|read-only)\b/i.test(head)) return { tier: 'research', why: 'research or audit', ...said };++ // A chore only when it reads like one and asks for nothing to be built (a short brief can still be a real feature).++ const builds = /\b(add|build|implement|create|endpoint|api|feature|refactor|migrat|tests?|component|page|service|integrat)\b/i.test(head);++ if (p.length < 1500 && !builds && /\b(rename|typo|reformat|format|bump|changelog|readme|docs? (update|fix)|add (a )?comment|count|list (all|the)|summari[sz]e|translate|check (that|whether|if)|lint|spell)\b/i.test(head)) return { tier: 'basic', why: 'a small chore', ...said };++ if (p.length < 200 && !builds) return { tier: 'basic', why: 'a short question', ...said };++ return { tier: 'standard', why: 'everyday code', ...said };++}++++/**++ * The tier of a job: from the brief its prompt names, or from the prompt itself.++ *++ * @param {string} prompt the launch prompt++ * @param {object} [opts]++ * @param {string} [opts.JOBS] the jobs folder, where briefs live++ * @param {string} [opts.name] the job's own name++ * @param {string} [opts.briefText] the brief's text, when the caller already has it++ * @returns {{tier: string, why: string, brief?: string}} the tier and the words that say so++ */++function classify(prompt, opts = {}) {++ const brief = opts.briefText ? { text: opts.briefText, file: opts.briefFile || null }++ : (opts.JOBS ? briefFor(prompt, opts.JOBS, opts.name) : null);++ if (!brief) return classifyText(String(prompt || ''));++ // The tier line of the brief wins wherever it stands in it.++ return classifyText(brief.text, { brief: brief.file || undefined, whole: true });++}++++/**++ * The record of what each runner really achieved: a runner whose recent jobs (7 days, at least 4) mostly failed for++ * real (not limits or sign-in, which the runner switch handles) is skipped until it does better.++ *++ * @param {object[]} runs the runs, newest first++ * @returns {object} per runner: { done, failed }++ */++function track(runs) {++ const since = Date.now() - 7 * 86400e3, rec = {};++ for (const r of runs || []) {++ if (r.session || r.runner === 'ship' || r.updated < since || !['done', 'failed', 'interrupted'].includes(r.status)) continue;++ if (r.status !== 'done' && ['limit', 'auth', 'capacity', 'refused'].includes(r.reason)) continue;++ const t = rec[r.runner] || (rec[r.runner] = { done: 0, failed: 0 });++ // Resolution belongs to the job chain, not this runner's attempt. A rescue++ // by another run (or an owner note) must not turn its failure into success.++ if (r.status === 'done') t.done++; else t.failed++;++ }++ return rec;++}++++/**++ * The runner, model and reasoning for a job, or null when nothing on the ladder can take it now.++ *++ * @param {string} prompt the brief or the launch prompt++ * @param {object} ctx++ * @param {object} ctx.cfg the config (subagents: per runner { enabled, max, model })++ * @param {object} ctx.health the health of each runner ({ state })++ * @param {object} ctx.counts how many jobs each runner runs now++ * @param {object} ctx.models the catalogs of each runner++ * @param {object[]} [ctx.runs] the recent runs, for the reliability gate++ * @param {string} [ctx.JOBS] the jobs folder++ * @param {string} [ctx.name] the job's own name++ * @param {object} [ctx.ladder] the ladder to use (the portal's by default)++ * @returns {{runner: string, model: string|null, effort: string|null, tier: string, brief: string|null, why: string}|null}++ */++function pick(prompt, { cfg, health, counts, models, runs, JOBS, name, ladder } = {}) {++ const c = classify(prompt, { JOBS, name });++ const rec = track(runs);++ const reliable = (r) => { const t = rec[r]; return !t || t.done + t.failed < 4 || t.done / (t.done + t.failed) >= 0.4; };++ const usable = (r) => cfg.subagents[r] && cfg.subagents[r].enabled && (counts[r] || 0) < cfg.subagents[r].max && health[r] && health[r].state === 'ok' && reliable(r);++ for (const [runner, model, wanted] of (ladder || LADDER)[c.tier]) {++ if (!usable(runner)) continue;++ // A model the runner no longer lists is skipped for that runner's default (lists refresh from each provider).++ const m = model && models[runner] && !models[runner].includes(model) ? null : model;++ const effort = fitEffort(runner, wanted);++ const judged = c.brief ? `${c.why} (the brief it names)` : c.why;++ return { runner, model: m, effort, tier: c.tier, brief: c.brief || null, why: `${judged} → ${runner}${m ? ' · ' + m : ''}${effort ? ' · ' + effort : ''}` };++ }++ return null;++}++++module.exports = { LADDER, TIERS, classify, classifyText, pick, track, briefFor, forcedTier, fitEffort, stepUp, EFFORTS };nnew file mode 100644@@@ -0,0 +1,53 @@++'use strict';++++/**++ * What a failed run really said.++ *++ * A harness that ends without an answer often leaves the wrapper's own no-result message behind++ * ("Command Code ended without a result (credits, rate limit or a crash: check its exit code)"). That message lists++ * possible causes, not an observed failure, so it is never reported as one: only a real diagnostic in the feed or on++ * stderr is. The result is the work's own error, never a fragment of the launch command (the brief is echoed after a++ * killed child).++ */++++/** The wrapper's no-result message: possible causes, not an observed quota failure. */++const NO_RESULT = /Command Code ended without a result\s*\(credits, rate limit or a crash: check its exit code\)/i;++++const MAX = 200;++++/** A message of any shape as one line of text. */++function message(value) {++ if (typeof value === 'string') return value;++ if (Array.isArray(value)) return value.map(message).filter(Boolean).join('; ');++ return value && typeof value === 'object' ? message(value.message || value.error) : '';++}++++/**++ * The error of a failed run, from its feed, its stderr and a summary.++ *++ * @param {string} feed the run's jsonl feed++ * @param {string} stderr what the launcher wrote++ * @param {string} summary a fallback message++ * @returns {string} the error, at most 200 characters ('' when there was none)++ */++function failureError(feed, stderr, summary = '') {++ let result = '';++ // Parse complete JSON records: escaped quotes and whitespace are legal, and tool output is not a run failure.++ for (const line of String(feed || '').split('\n')) {++ let e; try { e = JSON.parse(line); } catch { continue; }++ if (e.type === 'result' && e.is_error) result = message(e.result) || message(e.errors);++ }++ if (result && !NO_RESULT.test(result)) return result.trim().slice(0, MAX);++ const lines = String(stderr || '').split('\n');++ let diagnostic = '';++ for (const line of lines) {++ // Bash prints the entire launch command (including a multiline brief) after a killed child.++ // Keep only its signal diagnosis, never a fragment of the echoed command or brief.++ const signal = line.match(/:\s*line \d+:\s*\d+\s+(Killed|Terminated|Segmentation fault|Aborted)\b/);++ if (signal) return signal[1];++ if (/^(?:API Error:|Error:|Refused:|mcode exec (?:failed|cancelled):|\[claude-code:[\w-]+\]|(?:Killed|Terminated)\.?$)/.test(line.trim())) diagnostic = line.trim();++ }++ return (diagnostic || result || message(summary)).trim().slice(0, MAX);++}++++module.exports = { failureError, message, NO_RESULT };nnew file mode 100644@@@ -0,0 +1,117 @@++'use strict';++// Every job and chat turn runs in its own systemd scope inside ovjobs.slice (2026-10-30).++//++// Why: an agent that starts a server in the background (`node server/index.js &`, setsid, nohup) leaves it behind when++// its shell exits. The server is reparented to the user's systemd, so it is no longer in the job's process group or++// tree and procgroup.stopTree never sees it. Fifteen such servers and headless browsers (8-16 h old, ~7 GB) ran the++// workstation out of memory on 2026-10-30 01:38 UTC: the kernel killed the IDE, the portal and every running job.++//++// A scope is a cgroup: everything the job starts stays in it, however it detaches. When the job's own process is gone,++// reap() stops the scope, which ends whatever is left. ovjobs.slice caps all jobs together (MemoryHigh 15G, MemoryMax++// 18G) and each scope caps one job (6G/8G, OOMPolicy=continue: an out-of-memory kill takes one process, like a test++// runner, not the whole job), so no job can take the machine down again.++//++// `systemd-run --scope` execs the command in place: the pid, process group, cwd, env and stdio are the same as a++// plain spawn, so the rest of the runner (pid files, stopTree) is unchanged. Without systemd-run (or a user manager)++// wrap() returns the command as it was, and this module does nothing anywhere but Linux.++++const fs = require('fs');++const os = require('os');++const path = require('path');++const { execFile, execFileSync } = require('child_process');++++const SLICE = 'ovjobs.slice';++const PER_JOB = ['-p', 'MemoryHigh=6G', '-p', 'MemoryMax=8G', '-p', 'OOMPolicy=continue', '-p', 'TimeoutStopSec=15'];++++// Where the map of scopes lives: which process leads which scope, so reap() knows when a job itself is over.++const MAP = process.env.OV_JOB_SCOPE_MAP || path.join(os.homedir(), '.openvibe-codes', 'job-scopes.json');++++let usable = null;++++/** Are scopes available here? (Linux with a systemd user manager: false everywhere else.) */++function available() {++ if (usable !== null) return usable;++ if (process.platform !== 'linux') return (usable = false);++ if (process.env.OV_NO_JOB_SCOPES) return (usable = false);++ try {++ execFileSync('systemd-run', ['--user', '--scope', '--quiet', '--collect', `--slice=${SLICE}`, '--', 'true'], { timeout: 8000, stdio: 'ignore' });++ usable = true;++ } catch { usable = false; }++ return usable;++}++++const unitName = (label) => `ovjob-${String(label || 'job').replace(/[^A-Za-z0-9_-]+/g, '_').slice(0, 90)}-${Date.now().toString(36)}.scope`;++++/**++ * The command to spawn instead of (cmd, args), and the scope it will run in (null when scopes are not available).++ *++ * @param {string} cmd the command++ * @param {string[]} args its arguments++ * @param {string} label a name for the scope++ * @returns {{cmd: string, args: string[], unit: string|null}} what to spawn++ */++function wrap(cmd, args, label) {++ if (!available()) return { cmd, args, unit: null };++ const unit = unitName(label);++ return {++ cmd: 'systemd-run',++ args: ['--user', '--scope', '--quiet', '--collect', `--unit=${unit.replace(/\.scope$/, '')}`, `--slice=${SLICE}`, `--description=ovjob ${label}`, ...PER_JOB, '--', cmd, ...args],++ unit,++ };++}++++function load() { try { return JSON.parse(fs.readFileSync(MAP, 'utf8')); } catch { return {}; } }++function save(map) {++ try {++ fs.mkdirSync(path.dirname(MAP), { recursive: true });++ fs.writeFileSync(`${MAP}.tmp`, JSON.stringify(map));++ fs.renameSync(`${MAP}.tmp`, MAP);++ } catch { /* best effort */ }++}++++/** Record which process leads a scope, so reap() knows when the job itself is over. */++function remember(unit, pid, label) {++ if (!unit || !(pid > 1)) return;++ const map = load();++ map[unit] = { pid, label: label || '', at: Date.now() };++ save(map);++}++++const run = (args) => new Promise((resolve) => execFile('systemctl', ['--user', ...args], { timeout: 20000 }, (e, out) => resolve(e ? null : String(out || ''))));++++// The scope's leader is still the job: alive and still in that cgroup (a reused pid is somebody else).++function leads(pid, unit) {++ try { return fs.readFileSync(`/proc/${pid}/cgroup`, 'utf8').includes(`/${unit}`); } catch { return false; }++}++++/**++ * Stop every scope whose job has ended, ending what it left behind.++ *++ * @param {object} [opts]++ * @param {Function} [opts.log] where the notice goes++ * @returns {Promise<string[]>} the scopes stopped++ */++async function reap({ log = () => {} } = {}) {++ if (!available()) return [];++ const out = await run(['list-units', '--type=scope', '--plain', '--no-legend', '--all', 'ovjob-*']);++ if (out === null) return [];++ const live = new Set(out.split('\n').map((l) => l.trim().split(/\s+/)[0]).filter((u) => /^ovjob-.*\.scope$/.test(u)));++ const map = load();++ const stopped = [];++ for (const [unit, rec] of Object.entries(map)) {++ if (!live.has(unit)) { delete map[unit]; continue; } // gone already (nothing was left behind)++ if (leads(rec.pid, unit)) continue; // the job is still running++ if (await run(['stop', unit]) !== null) { stopped.push(unit); delete map[unit]; log(`jobscope: ${rec.label || unit} ended; stopped its scope (left-behind processes)`); }++ }++ save(map);++ return stopped;++}++++/** Stop one job's scope now (the owner pressed Stop, or the budget ended it). */++async function stopFor(pid) {++ const map = load();++ for (const [unit, rec] of Object.entries(map)) if (rec.pid === Number(pid)) { await run(['stop', unit]); delete map[unit]; save(map); return unit; }++ return null;++}++++module.exports = { available, wrap, remember, reap, stopFor, SLICE, MAP };nnew file mode 100644@@@ -0,0 +1,434 @@++'use strict';++++/**++ * One conversation shape for every harness's log, so a task reads the same whoever worked on it.++ *++ * The harnesses speak different languages: Claude Code stream-json (Claude, Claude on DeepSeek), the runners'++ * converted feeds (Codex, Command Code, OpenCode Go), DeepSeek's `dsh --json` and MiniMax Code's++ * `mcode exec --output-format stream-json`. This module turns all of them into one event shape:++ *++ * { t, text, name, input, output, at, … }++ *++ * `t` is the kind of event ('user', 'text', 'thinking', 'tool', 'error', 'final', 'meta'), `at` is when it happened++ * (ISO 8601), and a tool event carries its `name`, its `input` and, once it arrives, its `output`. The whole shape is++ * described in transcript.schema.json.++ *++ * Parsing is incremental per file: only the bytes added since the last read are parsed, so polling a running task++ * every second or two stays cheap. `parseText` does the same for a stream that is already in memory.++ */++++const fs = require('fs');++++const OUT_CAP = 8000;++const CACHE_BYTES = 32 * 1024 * 1024;++const cache = new Map();++let cacheBytes = 0;++++/** Forget a file's parsed state (its log was truncated or removed). */++function forget(file) {++ const entry = cache.get(file);++ if (entry) cacheBytes -= entry.bytes;++ cache.delete(file);++}++++/** Remember a file's parsed state, within the cache's size. */++function remember(file, state) {++ forget(file);++ const bytes = state.events.reduce((total, event) => total + JSON.stringify(event).length * 2 + 512, 1024)++ + state.byId.size * 128;++ if (bytes > CACHE_BYTES) return;++ while (cache.size >= 60 || cacheBytes + bytes > CACHE_BYTES) forget(cache.keys().next().value);++ cache.set(file, { state, bytes });++ cacheBytes += bytes;++}++++/** A value as text, clipped, with the ANSI colours stripped. */++function clip(v, n = OUT_CAP) {++ let s = typeof v === 'string' ? v : v == null ? '' : JSON.stringify(v, null, 2);++ s = s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');++ return s.length > n ? { text: s.slice(0, n), more: s.length - n } : { text: s, more: 0 };++}++++/** The kind of a tool, from its name. */++function kindOf(name) {++ const n = String(name || '').toLowerCase();++ if (/bash|shell|exec|command|terminal|run_?cmd/.test(n)) return 'shell';++ if (/^(read|view|cat|read_?file|open)/.test(n)) return 'read';++ if (/edit|write|patch|create_?file|replace|multiedit|str_replace/.test(n)) return 'edit';++ if (/grep|glob|search_?files|find|list|ls|rg/.test(n) && !/web/.test(n)) return 'search';++ if (/web|fetch|browse|url|http/.test(n)) return 'web';++ if (/todo|task|agent|plan/.test(n)) return 'plan';++ return 'tool';++}++++/** One line about what a tool was asked to do. */++function summary(name, input) {++ if (input == null) return '';++ if (typeof input !== 'object') return String(input).replace(/\s+/g, ' ').slice(0, 220);++ let v = input.command || input.cmd || input.file_path || input.filePath || input.path || input.pattern || input.query || input.url++ || input.description || input.prompt || Object.values(input).find((x) => typeof x === 'string');++ if (Array.isArray(v)) v = v.join(' ');++ v = String(v || '');++ // Codex wraps every command as /bin/bash -lc "…".++ const m = v.match(/^\/bin\/(?:ba)?sh -lc (['"])([\s\S]*)\1$/);++ if (m) v = m[2];++ // "cd <job folder> && …" says nothing new: show the command itself.++ v = v.replace(/^\s*cd\s+("[^"]+"|'[^']+'|\S+)\s*(&&|;)\s*/, '');++ return v.replace(/\s+/g, ' ').slice(0, 220);++}++++/** The text of a content block, of any shape. */++function textOf(c) {++ if (c == null) return '';++ if (typeof c === 'string') return c;++ if (Array.isArray(c)) return c.map((b) => (typeof b === 'string' ? b : b && (b.text || b.content || (b.type === 'image' ? '[image]' : '')) || '')).join('\n');++ if (typeof c === 'object') return c.text || c.content || c.output || JSON.stringify(c, null, 2);++ return String(c);++}++++/** A tool input as data, when it is a JSON string. */++function inputOf(raw) {++ if (typeof raw === 'string') { try { return JSON.parse(raw); } catch { return raw; } }++ return raw;++}++++/** An empty parse state. */++function freshState() {++ return { offset: 0, line: 0, events: [], byId: new Map(), session: null, model: null, cwd: null, usage: { input: 0, output: 0, cached: 0 }, cost: null };++}++++/** When an event happened, as ISO 8601. */++function atOf(ts) {++ if (ts == null || !Number.isFinite(Number(ts))) return null;++ try { return new Date(Number(ts)).toISOString(); } catch { return null; }++}++++/** Add an event to the state, merging the ones that belong together. */++function push(st, ev) {++ if (ev.at === undefined) ev.at = atOf(ev.ts);++ const last = st.events[st.events.length - 1];++ // The same error reported twice in a row (a system error, then the result) shows once.++ if (ev.t === 'error' && st.events.slice(-3).some((e) => e.t === 'error' && e.text.trim() === String(ev.text).trim())) return st.events[st.events.length - 1];++ // Consecutive thinking (dsh emits one per step) and streamed message deltas merge into one block.++ if (last && ev.t === last.t && !ev.mid && !last.mid && (ev.t === 'thinking' || (ev.t === 'text' && ev.delta && last.delta))) {++ last.text = (last.text + (ev.t === 'thinking' && !ev.delta ? '\n\n' : '') + ev.text).slice(-20000);++ return last;++ }++ ev.i = st.events.length;++ st.events.push(ev);++ return ev;++}++++/** Add a tool call. */++function addTool(st, id, name, input, line, ts) {++ const inp = inputOf(input);++ const ev = push(st, { t: 'tool', id: id || `t${st.events.length}`, name: name || 'tool', kind: kindOf(name), summary: summary(name, inp), input: clip(inp, 6000), pending: true, line, ts });++ st.byId.set(ev.id, ev.i);++ return ev;++}++++/** Add the result of a tool call. */++function setResult(st, id, output, isError) {++ const i = st.byId.get(id);++ if (i == null) return false;++ const ev = st.events[i];++ const out = clip(textOf(output));++ ev.output = out;++ ev.pending = false;++ ev.isError = !!isError || /\[exit code: [1-9]\d*\]|^Exit code [1-9]|\bexit_code["']?:\s*[1-9]/.test(out.text.slice(-200));++ const m = out.text.match(/\[exit code: (\d+)\]|Exit code (\d+)/);++ if (m) ev.exit = Number(m[1] || m[2]);++ ev.v = (ev.v || 0) + 1;++ return true;++}++++// A streamed piece of a message (Claude Code with --include-partial-messages): it grows in one place and is++// replaced by the whole message when that arrives, so the same words are never shown twice.++function pushDelta(st, ev) {++ for (let i = st.events.length - 1; i >= 0; i--) {++ const e = st.events[i];++ if (e.t !== ev.t) continue;++ if (e.partial) { e.text = (e.text + ev.text).slice(-20000); e.v = (e.v || 0) + 1; return e; }++ break;++ }++ // mid keeps the delta apart from the block before it (push merges two thinking blocks otherwise).++ const p = { ...ev, partial: true, mid: true };++ ev.i = st.events.length;++ st.events.push(p);++ return p;++}++++const lastPartial = (st, t) => {++ for (let i = st.events.length - 1; i >= 0; i--) if (st.events[i].t === t && st.events[i].partial) return st.events[i];++ return null;++};++++function finalEv(st, text, extra, line, ts) {++ let t = String(text || '').trim();++ if (t && st.events.slice(-4).some((e) => e.t === 'error' && e.text.trim() === t)) t = '';++ const last = [...st.events].reverse().find((e) => e.t === 'text');++ // Claude's result repeats its last message: mark that message as the answer instead of printing it twice.++ if (last && t && last.text.trim() === t) { last.answer = true; last.v = (last.v || 0) + 1; return push(st, { t: 'final', text: '', ...extra, line, ts }); }++ return push(st, { t: 'final', text: t, ...extra, line, ts });++}++++// MiniMax repeats a turn's usage in exec.completed, including partial usage when++// turn.failed has none. Account for each turn once, adding only newly reported tokens.++function minimaxUsage(st, e, payload = e) {++ const u = payload.usage;++ if (!u) return;++ const key = `${e.runId || payload.runId || ''}:${e.turnId || payload.turnId || ''}`;++ if (!st.minimaxUsage) st.minimaxUsage = new Map();++ const previous = st.minimaxUsage.get(key) || { input: 0, output: 0, cached: 0 };++ const cached = Number(u.cacheReadTokens || u.cachedInputTokens || u.cache_read_input_tokens || 0);++ const current = { input: Number(u.inputTokens || u.input_tokens || 0) + cached,++ output: Number(u.outputTokens || u.output_tokens || 0), cached };++ for (const field of ['input', 'output', 'cached']) {++ current[field] = Math.max(previous[field], current[field]);++ st.usage[field] += current[field] - previous[field];++ }++ st.minimaxUsage.set(key, current);++}++++/** Read one event of a harness' feed. */++function handle(st, e, line) {++ const ts = e._ts || e.timestampMs || (e.timestamp ? Date.parse(e.timestamp) : null) || null;++ const type = e.type;++ // MiniMax Code (schemaVersion 1 events).++ if (e.schemaVersion && typeof type === 'string' && /\./.test(type)) {++ if (e.sessionId) st.session = e.sessionId;++ // A tool call arrives as item.started / item.updated / item.completed with its input and output inside toolCall++ // (status 2 = done); tools run inside Programmatic Tool Calling's run_code carry parentToolCallId.++ if (/^item\./.test(type) && e.item && e.item.type === 'tool_call' && e.item.toolCall) {++ const tc = e.item.toolCall, id = tc.id || e.item.id;++ let i = st.byId.get(id);++ if (i == null) { const ev = addTool(st, id, tc.name || tc.toolName, tc.input || tc.arguments, line, ts); if (tc.parentToolCallId) ev.nested = true; i = ev.i; }++ const ev = st.events[i];++ if (tc.input != null && !ev.summary) { const inp = inputOf(tc.input); ev.input = clip(inp, 6000); ev.summary = summary(ev.name, inp); ev.v = (ev.v || 0) + 1; }++ if (tc.output != null || type === 'item.completed') setResult(st, id, tc.output != null ? (tc.output.content || tc.output) : '', tc.isError || tc.status === 3 || /fail|error/i.test(String(tc.status)));++ return;++ }++ // A message or thought takes its place when it starts (so it precedes the tools it announces) and its text when complete.++ if (type === 'item.started' && e.item && (e.item.type === 'agent_message' || e.item.type === 'reasoning')) {++ const ev = push(st, { t: e.item.type === 'agent_message' ? 'text' : 'thinking', text: '', mid: e.item.id, line, ts });++ st.byId.set('m:' + e.item.id, ev.i);++ return;++ }++ if (type === 'item.completed' && e.item && st.byId.has('m:' + e.item.id)) {++ const ev = st.events[st.byId.get('m:' + e.item.id)];++ ev.text = textOf(e.item.content || e.item.contentDelta) || ev.text; ev.v = (ev.v || 0) + 1;++ return;++ }++ if (type === 'item.completed' && e.item) {++ const it = e.item;++ if (it.type === 'agent_message') push(st, { t: 'text', text: textOf(it.content || it.contentDelta), line, ts });++ else if (it.type === 'reasoning') push(st, { t: 'thinking', text: textOf(it.content || it.contentDelta), line, ts });++ else if (/tool_result|tool_output/.test(it.type)) setResult(st, it.toolCallId || it.callId || it.id, it.output || it.content || it.result, it.isError || it.status === 'failed');++ } else if (type === 'turn.completed') {++ if (e.model && e.model.modelId) st.model = e.model.modelId;++ minimaxUsage(st, e);++ } else if (type === 'turn.failed') push(st, { t: 'error', text: textOf(e.error && (e.error.message || e.error)) || `turn ${e.status || 'failed'}`, line, ts });++ else if (type === 'exec.completed' && e.result) {++ if (e.result.model && e.result.model.modelId) st.model = e.result.model.modelId;++ minimaxUsage(st, e, e.result);++ finalEv(st, e.result.answer || e.result.output || '', { isError: e.result.status && e.result.status !== 'succeeded', ms: e.result.durationMs }, line, ts);++ }++ return;++ }++ // DeepSeek's official harness (dsh --json).++ if (type === 'session' && e.sessionId) { st.session = e.sessionId; st.cwd = e.cwd || st.cwd; push(st, { t: 'meta', kind: 'start', session: e.sessionId, cwd: e.cwd, line, ts }); return; }++ if (type === 'tool_call') { addTool(st, e.callId, e.tool || e.name, e.input || e.arguments, line, ts); return; }++ if (type === 'tool_result' && e.callId) { setResult(st, e.callId, e.result != null ? e.result : e.output, e.status && e.status !== 'completed'); return; }++ if (type === 'thinking') { push(st, { t: 'thinking', text: String(e.text || ''), line, ts }); return; }++ if (type === 'text' && typeof e.text === 'string') { push(st, { t: 'text', text: e.text, line, ts }); return; }++ if (type === 'final') { finalEv(st, e.text, {}, line, ts); return; }++ if (type === 'status') { if (e.usage && e.phase === 'step_end') { const c = Number(e.usage.cacheReadTokens || 0); st.usage.input += Number(e.usage.inputTokens || e.usage.promptTokens || 0) + c; st.usage.output += Number(e.usage.outputTokens || e.usage.completionTokens || 0); st.usage.cached += c; } return; }++ if (type === 'error') { push(st, { t: 'error', text: textOf(e.message || e.text || e.error), line, ts }); return; }++ // Claude Code stream-json and the runners' feeds (the same shape).++ if (type === 'system') {++ if (e.subtype === 'init') {++ st.session = e.session_id || st.session; st.model = e.model || st.model; st.cwd = e.cwd || st.cwd;++ push(st, { t: 'meta', kind: 'start', session: e.session_id, model: e.model, backend: e.backend, cwd: e.cwd, permission: e.permissionMode, line, ts });++ } else if (e.subtype === 'error') push(st, { t: 'error', text: String(e.text || e.message || 'error'), line, ts });++ else if (e.subtype === 'usage' && e.usage) { st.usage.input += Number(e.usage.input || 0); st.usage.output += Number(e.usage.output || 0); st.usage.cached += Number(e.usage.cached || 0); }++ else if (e.subtype === 'compact_boundary') push(st, { t: 'meta', kind: 'compact', line, ts });++ return;++ }++ // Claude Code's partial messages: the text and the thinking arrive as deltas while the turn runs.++ if (type === 'stream_event') {++ const ev = e.event || {};++ if (ev.type === 'content_block_delta') {++ const d = ev.delta || {};++ if (d.type === 'text_delta' && d.text) pushDelta(st, { t: 'text', text: d.text, line, ts });++ else if (d.type === 'thinking_delta' && (d.thinking || d.text)) pushDelta(st, { t: 'thinking', text: d.thinking || d.text, line, ts });++ }++ return;++ }++ if (type === 'assistant' && e.message) {++ if (e.message.model && e.message.model !== '<synthetic>') st.model = e.message.model; // Claude Code's own error messages say <synthetic>++ const u = e.message.usage;++ // Command Code's feed says includes_cache: input_tokens already holds the cached tokens, so adding them again++ // doubled its prompt volume and halved its shown cache rate (session-analysis 2026-10-29 §1.1).++ if (u) { const inc = u.includes_cache === true; st.usage.input += Number(u.input_tokens || 0) + (inc ? 0 : Number(u.cache_read_input_tokens || 0) + Number(u.cache_creation_input_tokens || 0)); st.usage.output += Number(u.output_tokens || 0); st.usage.cached += Number(u.cache_read_input_tokens || 0); }++ const content = Array.isArray(e.message.content) ? e.message.content : [{ type: 'text', text: textOf(e.message.content) }];++ for (const b of content) {++ if (!b) continue;++ if (b.type === 'text' && b.text && b.text.trim()) {++ // The whole message replaces what was streamed, so the words appear once.++ const p = lastPartial(st, 'text');++ if (p) { p.text = b.text; p.partial = false; p.v = (p.v || 0) + 1; }++ else push(st, { t: 'text', text: b.text, line, ts });++ }++ else if ((b.type === 'thinking' || b.type === 'reasoning') && (b.thinking || b.text)) {++ const p = lastPartial(st, 'thinking');++ if (p) { p.text = b.thinking || b.text; p.partial = false; p.v = (p.v || 0) + 1; }++ else push(st, { t: 'thinking', text: b.thinking || b.text, line, ts });++ }++ else if (b.type === 'tool_use') addTool(st, b.id, b.name, b.input, line, ts);++ }++ return;++ }++ if (type === 'user' && e.message) {++ const c = e.message.content;++ if (typeof c === 'string') { if (c.trim()) push(st, { t: 'user', text: c, line, ts }); return; }++ for (const b of c || []) {++ if (b && b.type === 'tool_result') setResult(st, b.tool_use_id, b.content, b.is_error);++ else if (b && b.type === 'text' && b.text && !/^<|^\[Request interrupted/.test(b.text.trim())) push(st, { t: 'user', text: b.text, line, ts });++ }++ return;++ }++ if (type === 'result') {++ if (e.session_id) st.session = e.session_id;++ if (e.total_cost_usd != null) st.cost = (st.cost || 0) + Number(e.total_cost_usd);++ const u = e.usage || {};++ const tokens = Number(u.input_tokens || u.inputTokens || 0) + Number(u.output_tokens || u.outputTokens || 0);++ finalEv(st, e.result, { isError: !!e.is_error, ms: e.duration_ms, turns: e.num_turns, cost: e.total_cost_usd, tokens: tokens || null }, line, ts);++ }++}++++/**++ * Parse a stream that is already in memory.++ *++ * @param {string} text the jsonl feed++ * @returns {object} the state: { events, session, model, cwd, usage, cost, line }++ */++function parseText(text) {++ const st = freshState();++ for (const l of String(text || '').split('\n')) {++ st.line++;++ if (!l || l[0] !== '{') continue;++ let e; try { e = JSON.parse(l); } catch { continue; }++ try { handle(st, e, st.line); } catch { /* one odd event never breaks the view */ }++ }++ return st;++}++++/**++ * Parse what was added to the file since the last call; returns the cached state.++ *++ * @param {string} file the jsonl feed++ * @returns {object} the state++ */++function parse(file) {++ const entry = cache.get(file);++ let st = entry && entry.state;++ let size = 0;++ try { size = fs.statSync(file).size; } catch { forget(file); return freshState(); }++ if (!st || size < st.offset) { forget(file); st = freshState(); }++ if (size === st.offset) {++ if (entry && entry.state === st) { cache.delete(file); cache.set(file, entry); }++ return st;++ }++ const fd = fs.openSync(file, 'r');++ try {++ // Read in chunks; stop at the last complete line.++ while (st.offset < size) {++ const len = Math.min(size - st.offset, 4 * 1024 * 1024);++ const buf = Buffer.alloc(len);++ fs.readSync(fd, buf, 0, len, st.offset);++ const end = buf.lastIndexOf(10);++ if (end < 0) {++ if (len === size - st.offset) break; // the last line is still being written++ st.offset += len; // a single line over 4 MB: skip it rather than stall++ continue;++ }++ for (const l of buf.subarray(0, end).toString('utf8').split('\n')) {++ st.line++;++ if (!l || l[0] !== '{') continue;++ let e; try { e = JSON.parse(l); } catch { continue; }++ try { handle(st, e, st.line); } catch { /* one odd event never breaks the view */ }++ }++ st.offset += end + 1;++ }++ } finally { fs.closeSync(fd); }++ remember(file, st);++ return st;++}++++/**++ * The events from index `from`, plus where the client should ask from next time (the first tool still waiting).++ *++ * @param {object} st the state++ * @param {number} [from] the first event wanted++ * @param {number} [limit] how many events at most++ * @returns {object} { events, from, total, skipped, next, info }++ */++function view(st, from = 0, limit = 600) {++ const total = st.events.length;++ const open = st.events.findIndex((e) => e.t === 'tool' && e.pending);++ let start = Math.max(0, Math.min(from, total));++ let skipped = 0;++ if (start === 0 && total > limit) { skipped = total - limit; start = skipped; }++ return {++ events: st.events.slice(start),++ from: start, total, skipped,++ next: open >= 0 ? Math.min(open, total) : total,++ info: { session: st.session, model: st.model, cwd: st.cwd, usage: st.usage, cost: st.cost },++ };++}++++// The few numbers a list of jobs needs from a log, memoised by the file's size and mtime: an unchanged log is never++// parsed again (the full parse cache above holds 60 files, and a busy day has hundreds). persistStats(file) keeps the++// memo across restarts.++const STATS = new Map();++let statsFile = null, statsDirty = false;++++/** The numbers of a parsed stream: tools, edits, errors, tokens, cost. */++function statsOf(st) {++ const u = st.usage || { input: 0, output: 0, cached: 0 };++ let tools = 0, edits = 0, errs = 0, lastTool = '', lastErr = '';++ for (const e of st.events) {++ if (e.t === 'tool') { tools++; if (e.kind === 'edit') edits++; if (e.isError) errs++; lastTool = `${e.name} ${e.summary}`; }++ if (e.t === 'error' || (e.t === 'final' && e.isError && e.text)) lastErr = String(e.text).slice(0, 200);++ }++ return {++ tools, edits, errs, cost: Number.isFinite(st.cost) ? Number(st.cost) : null,++ input: u.input || 0, output: u.output || 0, cached: u.cached || 0,++ model: st.model || null, lastTool: lastTool.slice(0, 200), lastErr,++ };++}++++/** The numbers of a log, memoised by the file's size and mtime. */++function stats(file) {++ let stt; try { stt = fs.statSync(file); } catch { return null; }++ const hit = STATS.get(file);++ if (hit && hit.size === stt.size && hit.mtime === stt.mtimeMs) return hit.s;++ const s = statsOf(parse(file));++ STATS.set(file, { size: stt.size, mtime: stt.mtimeMs, s });++ statsDirty = true;++ return s;++}++++/** Keep the memo across restarts, in a file of the caller's choosing. */++function persistStats(file) {++ statsFile = file;++ try { for (const [k, v] of Object.entries(JSON.parse(fs.readFileSync(file, 'utf8')))) STATS.set(k, v); } catch { /* first run */ }++ setInterval(() => {++ if (!statsDirty || !statsFile) return;++ statsDirty = false;++ for (const k of STATS.keys()) if (!fs.existsSync(k)) STATS.delete(k);++ try { fs.writeFileSync(`${statsFile}.tmp`, JSON.stringify(Object.fromEntries(STATS))); fs.renameSync(`${statsFile}.tmp`, statsFile); } catch { statsDirty = true; }++ }, 30000).unref();++}++++module.exports = { parse, parseText, view, summary, kindOf, textOf, clip, stats, statsOf, persistStats, atOf };nnew file mode 100644@@@ -0,0 +1,159 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.codes/schemas/engine/transcript@1",++ "title": "OpenVibe.Codes transcript",++ "description": "One event shape for every harness's log: Claude Code stream-json, Codex, Command Code, OpenCode, dsh --json and MiniMax Code stream-json. The normaliser lives in engine/transcript.js.",++ "type": "object",++ "required": ["events"],++ "properties": {++ "events": {++ "type": "array",++ "description": "The conversation, in order.",++ "items": { "$ref": "#/$defs/event" }++ },++ "session": {++ "type": ["string", "null"],++ "description": "The harness's session id, when it reported one (a continue needs it)."++ },++ "model": {++ "type": ["string", "null"],++ "description": "The model that worked on it, when the feed said."++ },++ "cwd": {++ "type": ["string", "null"],++ "description": "The folder it worked in."++ },++ "usage": {++ "type": "object",++ "description": "The tokens it used.",++ "properties": {++ "input": { "type": "number", "minimum": 0 },++ "output": { "type": "number", "minimum": 0 },++ "cached": { "type": "number", "minimum": 0 }++ },++ "additionalProperties": false++ },++ "cost": {++ "type": ["number", "null"],++ "description": "The dollars the feed reported, when it reported any."++ },++ "line": {++ "type": "integer",++ "minimum": 0,++ "description": "The number of feed lines that were read."++ }++ },++ "additionalProperties": true,++ "$defs": {++ "event": {++ "type": "object",++ "required": ["t"],++ "description": "One event of the conversation.",++ "properties": {++ "t": {++ "type": "string",++ "enum": ["user", "text", "thinking", "tool", "error", "final", "meta"],++ "description": "The kind of event."++ },++ "text": {++ "type": "string",++ "description": "The words: what was said, thought, or went wrong."++ },++ "name": {++ "type": "string",++ "description": "The tool's name (on a tool event)."++ },++ "kind": {++ "type": "string",++ "enum": ["shell", "read", "edit", "search", "web", "plan", "tool"],++ "description": "What kind of tool it was (on a tool event)."++ },++ "summary": {++ "type": "string",++ "description": "One line about what the tool was asked to do."++ },++ "input": {++ "type": "object",++ "description": "What the tool was given.",++ "properties": {++ "text": { "type": "string" },++ "more": { "type": "number", "minimum": 0 }++ },++ "required": ["text"],++ "additionalProperties": true++ },++ "output": {++ "type": ["object", "null"],++ "description": "What the tool answered, once it did.",++ "properties": {++ "text": { "type": "string" },++ "more": { "type": "number", "minimum": 0 }++ },++ "required": ["text"],++ "additionalProperties": true++ },++ "at": {++ "type": ["string", "null"],++ "format": "date-time",++ "description": "When it happened (ISO 8601), when the feed said."++ },++ "ts": {++ "type": ["number", "null"],++ "description": "When it happened, in milliseconds since the epoch."++ },++ "line": {++ "type": "integer",++ "minimum": 0,++ "description": "The feed line it came from."++ },++ "i": {++ "type": "integer",++ "minimum": 0,++ "description": "Its index in the event list."++ },++ "id": {++ "type": "string",++ "description": "The tool call's id (on a tool event)."++ },++ "pending": {++ "type": "boolean",++ "description": "The tool has not answered yet."++ },++ "isError": {++ "type": "boolean",++ "description": "It failed."++ },++ "exit": {++ "type": "number",++ "description": "The exit code of the command (on a tool event)."++ },++ "v": {++ "type": "integer",++ "minimum": 0,++ "description": "How often this event changed."++ },++ "partial": {++ "type": "boolean",++ "description": "The words are still streaming in."++ },++ "answer": {++ "type": "boolean",++ "description": "This message is the answer of the turn."++ },++ "session": {++ "type": "string",++ "description": "The session id (on a meta event)."++ },++ "model": {++ "type": "string",++ "description": "The model (on a meta event)."++ },++ "cwd": {++ "type": "string",++ "description": "The folder (on a meta event)."++ }++ },++ "additionalProperties": true++ }++ }++}nnew file mode 100644@@@ -0,0 +1,70 @@++'use strict';++// The per-task budget, ported from the portal's jobbudget.js: steps, minutes and dollars, and the wrap-up turn that++// lands the work when the steps or the minutes run out. Dollars stay a hard stop.++++const assert = require('node:assert/strict');++const { check, done } = require('./helpers/boot');++const { limitsFor, evaluate, state, wrapUp, wrapUpPrompt, STATES } = require('../engine/budget');++++(async () => {++ await check('the limits are the config\'s, then the runner\'s, then the task\'s', () => {++ const cfg = { budget: { steps: 100, minutes: 60, usd: 5, perRunner: { cc: { steps: 50 } } } };++ assert.deepEqual(limitsFor(cfg, 'mm'), { steps: 100, minutes: 60, usd: 5 });++ assert.deepEqual(limitsFor(cfg, 'cc'), { steps: 50, minutes: 60, usd: 5 });++ assert.deepEqual(limitsFor(cfg, 'mm', { steps: 10, usd: 1 }), { steps: 10, minutes: 60, usd: 1 });++ assert.deepEqual(limitsFor({}, 'mm'), { steps: null, minutes: null, usd: null });++ });++++ await check('a limit that is not set is no limit, never a zero budget', () => {++ assert.deepEqual(limitsFor({ budget: { steps: 0, minutes: -1, usd: 'many' } }, 'mm'), { steps: null, minutes: null, usd: null });++ assert.equal(evaluate({ steps: 100000 }, limitsFor({}, 'mm')).hit, null);++ });++++ await check('the first limit reached wins', () => {++ const limits = { steps: 100, minutes: 60, usd: 5 };++ assert.equal(evaluate({ steps: 120, minutes: 90, usd: 9 }, limits).hit, 'steps');++ assert.equal(evaluate({ steps: 10, minutes: 90, usd: 9 }, limits).hit, 'minutes');++ assert.equal(evaluate({ steps: 10, minutes: 20, usd: 9 }, limits).hit, 'usd');++ assert.equal(evaluate({ steps: 10, minutes: 20, usd: 1 }, limits).hit, null);++ });++++ await check('an unknown spend is not a reason to stop', () => {++ assert.equal(evaluate({ steps: 1, minutes: null, usd: null }, { steps: 100, minutes: 60, usd: 5 }).hit, null);++ const out = evaluate({ steps: 1, minutes: null, usd: null }, { steps: 100, minutes: 60, usd: 5 });++ assert.equal(out.used.minutes, null);++ assert.equal(out.used.usd, null);++ });++++ await check('a task that stopped on a limit is budget-stopped, never a vague failure', () => {++ assert.equal(state('budget-stopped'), 'budget-stopped');++ assert.equal(state('failed'), 'failed');++ assert.equal(state('interrupted'), 'failed');++ assert.equal(state('done'), 'done');++ assert.equal(state('running'), 'running');++ assert.ok(STATES.includes('budget-stopped'));++ });++++ await check('steps and minutes buy one wrap-up turn', () => {++ const steps = wrapUp('steps');++ assert.equal(steps.action, 'wrap-up');++ assert.match(steps.prompt, /one short last turn/);++ assert.match(steps.prompt, /Do not start anything new/);++ assert.equal(wrapUp('minutes').action, 'wrap-up');++ });++++ await check('a dollar limit is a hard stop', () => {++ const dollars = wrapUp('usd');++ assert.equal(dollars.action, 'stop');++ assert.equal(dollars.prompt, null);++ });++++ await check('only one wrap-up turn', () => {++ assert.equal(wrapUp('steps', { done: true }).action, 'stop');++ });++++ await check('the wrap-up prompt says which limit was reached', () => {++ assert.match(wrapUpPrompt('minutes'), /your minutes limit/);++ });++++ done();++})();nnew file mode 100644@@@ -0,0 +1,162 @@++'use strict';++// The chain of one task, ported from the portal's test-chain-end.js: a failed job's line followed one hop, and a++// step-limit stop with nothing after it was never attention. The chain is followed to the run that ended it: through++// retry links, through a session that carried a run on (its parent), and through what resolved the next hop.++//++// The portal read those links from marker files in its log folder; the engine is pure, so the links come in with the++// attempts.++++const assert = require('node:assert/strict');++const { check, done } = require('./helpers/boot');++const chain = require('../engine/chain');++++const now = Date.now();++const job = (runner, name, status, at, extra = {}) => ({++ key: `${runner}:${name}`, runner, name, runnerName: runner.toUpperCase(),++ status, updated: at, error: status === 'done' ? '' : 'it broke', ...extra,++});++const carried = (parent, name, at) => ({++ key: `cd:${name}`, runner: 'mm', harness: 'mm', parent, runnerName: 'MiniMax Code',++ name: `↻ ${name}`, status: 'done', updated: at, error: '', session: true,++});++++(async () => {++ await check('a chain is followed through its retries and the sessions that carried it on', () => {++ const attempts = [++ // a: crashed, its retry stopped at the step limit, and a session carried that retry on to the end.++ job('cc', 'a', 'failed', now - 9000, { next: 'mm:a-r2' }),++ job('mm', 'a-r2', 'budget-stopped', now - 5000),++ carried('mm:a-r2', 'a-r2', now - 1000),++ // b: crashed, its retry stopped at the step limit, and nothing carried it on.++ job('cx', 'b', 'failed', now - 9000, { next: 'mm:b-r2' }),++ job('mm', 'b-r2', 'budget-stopped', now - 5000),++ // c: the same as b, but the owner marked the stop handled.++ job('cc', 'c', 'failed', now - 9000, { next: 'mm:c-r2' }),++ job('mm', 'c-r2', 'budget-stopped', now - 5000, { resolvedNote: 'finished by hand in PR 12' }),++ // d: crashed, its retry crashed too, and the second retry finished.++ job('cc', 'd', 'failed', now - 9000, { next: 'mm:d-r2' }),++ job('mm', 'd-r2', 'failed', now - 5000, { next: 'cm:d-r3' }),++ job('cm', 'd-r3', 'done', now - 1000),++ ];++ const runs = chain.annotate(attempts);++ const by = Object.fromEntries(runs.map((r) => [r.key, r]));++++ assert.equal(by['cc:a'].resolved.how, 'done', 'a session that carried the retry on finished the chain');++ assert.equal(by['cc:a'].resolved.byName, '↻ a-r2');++ assert.equal(by['mm:a-r2'].resolved.how, 'done', 'the step-limit stop it carried on is resolved too');++++ assert.equal(by['cx:b'].resolved.how, 'budget', 'the chain ended in a step-limit stop');++ assert.match(by['cx:b'].resolved.note, /b-r2, which stopped at its step limit before it finished/);++ assert.doesNotMatch(by['cx:b'].resolved.note, /failed again/, 'a step-limit stop is not called a failure');++ assert.equal(by['mm:b-r2'].resolved, undefined, 'the unfinished stop needs attention');++ assert.equal(by['mm:b-r2'].reason, 'budget');++++ assert.equal(by['mm:c-r2'].resolved.how, 'note', 'a stop the owner marked handled is handled');++ assert.equal(by['cc:c'].resolved.how, 'note', 'and so is the job it carried on');++ assert.match(by['cc:c'].resolved.note, /finished by hand in PR 12/);++++ assert.equal(by['cc:d'].resolved.how, 'done', 'two hops down, the chain finished');++ assert.equal(by['cc:d'].resolved.byName, 'd-r3');++ assert.equal(by['mm:d-r2'].resolved.how, 'done');++ });++++ await check('a handled note that names where the work went is followed to how that job ended', () => {++ const attempts = [++ job('cc', 'e', 'failed', now - 9000, { next: 'mm:e-r2' }),++ job('mm', 'e-r2', 'budget-stopped', now - 5000, { resolvedNote: 'Superseded: the commits are being integrated by mm:integrate-x.' }),++ job('mm', 'integrate-x', 'budget-stopped', now - 3000),++ carried('mm:integrate-x', 'integrate-x', now - 1000),++ ];++ const m = Object.fromEntries(chain.annotate(attempts).map((r) => [r.key, r]));++ assert.equal(m['mm:e-r2'].resolved.how, 'done', 'the note named the job that finished the work');++ assert.equal(m['mm:e-r2'].resolved.byName, '↻ integrate-x');++ assert.match(m['mm:e-r2'].resolved.said, /being integrated by mm:integrate-x/, 'the note is kept');++ assert.equal(m['cc:e'].resolved.how, 'done');++ });++++ await check('the attempts are not changed by annotating them', () => {++ const attempts = [job('cc', 'a', 'failed', now - 9000, { next: 'mm:a-r2' }), job('mm', 'a-r2', 'budget-stopped', now - 5000)];++ const before = JSON.stringify(attempts);++ chain.annotate(attempts);++ assert.equal(JSON.stringify(attempts), before, 'the input is never changed');++ });++++ await check('the outcome of a task is its state, its reason and its next step', () => {++ const attempts = [++ job('cc', 'a', 'failed', now - 9000, { error: 'usage limit reached' }),++ job('mm', 'a-r2', 'done', now - 5000),++ ];++ const out = chain.outcome(attempts);++ assert.equal(out.state, 'done');++ assert.equal(out.end.key, 'mm:a-r2');++ assert.equal(out.step.action, 'done');++ assert.equal(out.attempts.length, 2);++ });++++ await check('a limit hands the task to another runner', () => {++ const cfg = { subagents: { cc: { enabled: true, max: 2 }, mm: { enabled: true, max: 2 } }, fallback: { order: ['cc', 'mm'] } };++ const ctx = { cfg, health: { cc: { state: 'ok' }, mm: { state: 'ok' } }, counts: {}, models: {}, runners: ['cc', 'mm'] };++ const attempts = [job('cc', 'a', 'failed', now, { error: 'usage limit reached' })];++ const step = chain.nextStep(attempts, ctx);++ assert.equal(step.action, 'switch');++ assert.equal(step.runner, 'mm');++ assert.match(step.why, /hit its usage limit/);++ });++++ await check('a real error is retried on another runner, one reasoning level up', () => {++ const cfg = { subagents: { cc: { enabled: true, max: 2 }, mm: { enabled: true, max: 2 } }, fallback: { order: ['cc', 'mm'] } };++ const ctx = { cfg, health: { cc: { state: 'ok' }, mm: { state: 'ok' } }, counts: {}, models: {}, runners: ['cc', 'mm'] };++ const attempts = [job('cc', 'a', 'failed', now, { error: 'the tests did not pass', effort: 'medium' })];++ const step = chain.nextStep(attempts, ctx);++ assert.equal(step.action, 'switch');++ assert.equal(step.runner, 'mm');++ assert.equal(step.effort, 'high', 'one level up: the same attempt again rarely helps');++ });++++ await check('a crash is continued in the same session', () => {++ const attempts = [{ ...job('cc', 'a', 'interrupted', now), facts: { session: 'sess-1', exit: 137 } }];++ const step = chain.nextStep(attempts);++ assert.equal(step.action, 'continue');++ assert.equal(step.session, 'sess-1');++ assert.match(step.why, /crash/);++ });++++ await check('a context that grew too big starts fresh instead of continuing', () => {++ const attempts = [{ ...job('cc', 'a', 'failed', now), facts: { apiError: 'prompt is too long: 250000 tokens' } }];++ const step = chain.nextStep(attempts);++ assert.equal(step.action, 'restart');++ assert.match(step.why, /context grew too big/);++ });++++ await check('a stop is not a failure and is never handed to another runner', () => {++ const attempts = [job('cc', 'a', 'stopped', now)];++ const step = chain.nextStep(attempts, { cfg: { subagents: {}, fallback: { order: [] } } });++ assert.equal(step.action, 'stop');++ });++++ await check('a step limit buys one wrap-up turn, a dollar limit does not', () => {++ const steps = [job('cc', 'a', 'budget-stopped', now, { budget: { hit: 'steps' } })];++ assert.equal(chain.nextStep(steps).action, 'wrap-up');++ assert.match(chain.nextStep(steps).prompt, /one short last turn/);++ const dollars = [job('cc', 'a', 'budget-stopped', now, { budget: { hit: 'usd' } })];++ assert.equal(chain.nextStep(dollars).action, 'stop');++ const done = [job('cc', 'a', 'budget-stopped', now, { budget: { hit: 'steps' }, wrapUp: true })];++ assert.equal(chain.nextStep(done).action, 'stop', 'only one wrap-up turn');++ });++++ await check('a running task is waited for, not decided', () => {++ assert.equal(chain.nextStep([job('cc', 'a', 'running', now)]).action, 'wait');++ assert.equal(chain.nextStep([job('cc', 'a', 'done', now)]).action, 'done');++ });++++ await check('the names of one job, its chain and its base are told apart', () => {++ assert.equal(chain.base('bench-7-finish'), 'bench7');++ assert.equal(chain.base('bench7-r2'), 'bench7');++ assert.equal(chain.chainRoot('x-r2'), 'x');++ assert.equal(chain.chainRoot('x-c1'), 'x');++ assert.equal(chain.sameChain('x', 'x-b'), true);++ assert.equal(chain.sameChain('seo-batch-a', 'seo-batch-b'), false);++ });++++ done();++})();nnew file mode 100644@@@ -0,0 +1,86 @@++'use strict';++// The reasoning level a harness accepts, ported from the portal's test-effort-fit.js. The 2026-10-29 incident: a job++// retried on another runner copied cx's "medium" to Command Code, which died before its first step ("Unknown effort++// \"medium\". Supported: off, low, high, max."). Levels are also uneven (off and ultra exist on one harness only), so++// the ordering must know them all or a cross-runner value passes through untouched.++//++// The blocked-model list, from the portal's test-blocked-models.js: a model the provider refuses is replaced by one it++// still lists, and a launch with nothing left is an error, never a silent run with a refused model.++++const assert = require('node:assert/strict');++const { check, done } = require('./helpers/boot');++const { fitEffort, stepUp, EFFORTS, pickModel, blockModel, unblockModel, isBlocked, harnessOf } = require('../engine/effort');++++const ALL = ['off', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'];++++(async () => {++ await check('every level a harness offers is a level the ordering knows', () => {++ for (const [harness, list] of Object.entries(EFFORTS)) {++ for (const level of list) assert.ok(ALL.includes(level), `${harness} offers ${level}, which fitEffort does not order`);++ }++ });++++ await check('whatever comes in, the result is accepted by the harness', () => {++ for (const [harness, list] of Object.entries(EFFORTS)) {++ for (const level of ALL) {++ const got = fitEffort(harness, level);++ if (list.length) assert.ok(list.includes(got), `${harness} would be handed ${got} for ${level}`);++ else assert.equal(got, level, `${harness} has no effort setting: ${level} passes through`);++ }++ }++ });++++ await check('the reported case and the neighbours it must not break', () => {++ assert.equal(fitEffort('cc', 'medium'), 'high');++ assert.equal(fitEffort('cc', 'xhigh'), 'max');++ assert.equal(fitEffort('cc', 'ultra'), 'max');++ assert.equal(fitEffort('cc', 'off'), 'off');++ assert.equal(fitEffort('cx', 'off'), 'low');++ assert.equal(fitEffort('dsh', 'medium'), 'high');++ assert.equal(fitEffort('cc', 'high'), 'high', 'an accepted level is left alone');++ assert.equal(fitEffort('cc', null), null);++ });++++ await check('Codes\' harness names are the same harnesses', () => {++ assert.equal(harnessOf('command-code'), 'cc');++ assert.equal(harnessOf('codex'), 'cx');++ assert.equal(harnessOf('opencode'), 'oc');++ assert.equal(harnessOf('claude-code'), 'claude');++ assert.equal(harnessOf('deepseek'), 'dsh');++ assert.equal(fitEffort('command-code', 'medium'), 'high');++ assert.equal(fitEffort('claude-code', 'high'), 'high');++ });++++ await check('a retry goes one level up', () => {++ assert.equal(stepUp('medium', EFFORTS.cc), 'high');++ assert.equal(stepUp('max', EFFORTS.cc), 'max');++ assert.equal(stepUp('medium', EFFORTS.cx), 'high');++ });++++ await check('a blocked model is replaced by one the harness still lists', () => {++ const blockedModels = { cx: ['gpt-6.1-sol', 'gpt-6.1-terra'] };++ const models = { cx: ['gpt-6.1-sol', 'gpt-6-sol'] };++ assert.equal(pickModel({ runner: 'cx', model: 'gpt-6.1-sol', models, blockedModels }), 'gpt-6-sol');++ assert.equal(pickModel({ runner: 'cx', model: 'gpt-6.1-terra', models, blockedModels }), 'gpt-6-sol');++ assert.equal(pickModel({ runner: 'cx', model: 'gpt-6-sol', models, blockedModels }), 'gpt-6-sol', 'an unblocked model is left alone');++ assert.equal(pickModel({ runner: 'cx', model: null, models, blockedModels }), null, 'no model asked for, none forced');++ });++++ await check('a launch with nothing unblocked left is an error', () => {++ const blockedModels = { cx: ['gpt-6.1-sol', 'gpt-6-sol'] };++ const models = { cx: ['gpt-6.1-sol', 'gpt-6-sol'] };++ assert.throws(() => pickModel({ runner: 'cx', model: 'gpt-6.1-sol', models, blockedModels }), /No unblocked model/);++ });++++ await check('a model is blocked only when the provider says it does not have it', () => {++ assert.deepEqual(blockModel([], 'gpt-6.1-sol', 'model_not_found'), ['gpt-6.1-sol']);++ assert.deepEqual(blockModel([], 'gpt-6.1-sol', 'not available on your plan'), ['gpt-6.1-sol']);++ assert.deepEqual(blockModel([], 'gpt-6.1-sol', 'the connection was reset'), [], 'a crash is not a missing model');++ assert.deepEqual(blockModel(['gpt-6.1-sol'], 'gpt-6.1-sol', 'model_not_found'), ['gpt-6.1-sol'], 'no twice');++ assert.deepEqual(unblockModel(['a', 'b'], 'a'), ['b']);++ assert.equal(isBlocked({ cx: ['x'] }, 'cx', 'x'), true);++ assert.equal(isBlocked({ cx: ['x'] }, 'cx', 'y'), false);++ });++++ done();++})();nnew file mode 100644@@@ -0,0 +1,43 @@++'use strict';++// The failure reasons, ported from the portal's test-failure-reason.js: why a run failed, and what that means for the++// routing. A limit, a lost sign-in, a busy model and a runner turned off are the runner switch's business; a real++// error is not (and must not be mistaken for one).++++const assert = require('node:assert/strict');++const { check, done } = require('./helpers/boot');++const { reason, SWITCHABLE } = require('../engine/chain');++const { track } = require('../engine/router');++++const generic = 'API Error: Command Code ended without a result (credits, rate limit or a crash: check its exit code)';++const CASES = [++ [generic, 'error'],++ [generic + '\nAPI Error: usage limit reached', 'limit'],++ ['mcode exec failed: The run failed: Internal server error. Please retry your request. (500). Retry after the connection recovers.', 'capacity'],++ ['HTTP 502 Bad Gateway', 'capacity'], ['status code: 504', 'capacity'],++ ['Request rejected (429)', 'limit'], ['HTTP 429', 'limit'],++ ['API key has expired', 'auth'], ['model at capacity', 'capacity'],++ ['turned off in the agent portal', 'refused'],++ ['AssertionError: expected 500 items, got 429', 'error'],++ ['test failed: expected HTTP 200, got 404', 'error'],++];++++(async () => {++ await check('every error is classified', () => {++ for (const [text, expected] of CASES) assert.equal(reason(text), expected, text);++ assert.equal(reason(''), 'unknown');++ });++++ await check('a provider outage is the runner switch\'s business', () => {++ assert.equal(SWITCHABLE.has(reason(CASES[2][0])), true, 'provider outages use existing fallback');++ });++++ await check('an unknown failure is not excluded as a quota failure', () => {++ assert.deepEqual(track([{ runner: 'cc', status: 'failed', updated: Date.now(), reason: reason(generic) }]), { cc: { done: 0, failed: 1 } }, 'unknown failures must not be excluded as quota failures');++ });++++ await check('a provider outage does not penalise a runner\'s reliability', () => {++ assert.deepEqual(track([{ runner: 'mm', status: 'failed', updated: Date.now(), reason: reason(CASES[2][0]) }]), {}, 'provider outages do not penalize coding reliability');++ });++++ done();++})();nnew file mode 100644@@@ -0,0 +1,45 @@++'use strict';++// The engine as a package: what it exports, and what it may depend on.++++const assert = require('node:assert/strict');++const fs = require('node:fs');++const path = require('node:path');++const { check, done } = require('./helpers/boot');++const engine = require('../engine');++++const DIR = path.join(__dirname, '..', 'engine');++++(async () => {++ await check('the engine exports every module and the names a caller wants', () => {++ for (const name of ['router', 'chain', 'budget', 'transcript', 'effort', 'scope', 'runerror']) {++ assert.ok(engine[name], `${name} is exported`);++ }++ for (const name of ['classify', 'pick', 'outcome', 'nextStep', 'evaluate', 'limitsFor', 'parse', 'parseText', 'fitEffort', 'pickModel']) {++ assert.equal(typeof engine[name], 'function', `${name} is a function`);++ }++ });++++ await check('the engine needs nothing but node itself', () => {++ const builtins = new Set(require('node:module').builtinModules);++ for (const file of fs.readdirSync(DIR)) {++ if (!file.endsWith('.js')) continue;++ const source = fs.readFileSync(path.join(DIR, file), 'utf8');++ for (const m of source.matchAll(/require\('([^']+)'\)/g)) {++ const name = m[1];++ if (name.startsWith('.')) continue;++ assert.ok(builtins.has(name.replace(/^node:/, '')), `${file} requires ${name}, which is not a node builtin`);++ }++ }++ });++++ await check('every module of the engine has a file and a test', () => {++ const files = fs.readdirSync(DIR).filter((f) => f.endsWith('.js')).map((f) => f.replace(/\.js$/, ''));++ const tests = fs.readdirSync(__dirname).filter((f) => f.startsWith('engine-') && f.endsWith('.test.js'))++ .map((f) => f.replace(/^engine-/, '').replace(/\.test\.js$/, ''));++ for (const name of files) {++ assert.ok(tests.includes(name.replace(/-/g, '-')), `${name} has a test`);++ }++ });++++ done();++})();nnew file mode 100644@@@ -0,0 +1,113 @@++'use strict';++// The tier router, ported from the portal's test-router-brief.js and test-router-outcomes.js.++//++// Bug 3: the router judges a job by its launch prompt. A prompt that only says "read /…/x.brief.md and follow it" was++// routed as "a short question" to a free model, so a hard job ran on a cheap one. The router now classifies on the++// brief the prompt names (and on the job's own brief), and honours the tier line of that brief.++++const assert = require('node:assert/strict');++const fs = require('node:fs');++const os = require('node:os');++const path = require('node:path');++const { check, done } = require('./helpers/boot');++const router = require('../engine/router');++++const folder = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-router-'));++const JOBS = path.join(folder, 'jobs');++fs.mkdirSync(JOBS);++++(async () => {++ try {++ const hard = 'Root cause of the race condition in the payment webhook.\n\n'.repeat(40) + '\ntier: hard\n';++ fs.writeFileSync(path.join(JOBS, 'pin-round.brief.md'), hard);++ const pointer = `Read ${path.join(JOBS, 'pin-round.brief.md')} first and follow it exactly.`;++++ await check('the prompt alone is judged as it reads', () => {++ assert.equal(router.classify(pointer).tier, 'basic');++ assert.equal(router.classify(pointer).why, 'a short question');++ });++++ await check('the brief a prompt names decides the tier', () => {++ const judged = router.classify(pointer, { JOBS, name: 'pin-round' });++ assert.equal(judged.tier, 'hard', 'the brief decides the tier');++ assert.equal(judged.brief, path.join(JOBS, 'pin-round.brief.md'));++ });++++ await check('the tier line of the brief is honoured wherever it stands in it', () => {++ fs.writeFileSync(path.join(JOBS, 'chore.brief.md'), 'Rename a variable in one file.\n\ntier: basic\n');++ assert.equal(router.classify(`Follow ${path.join(JOBS, 'chore.brief.md')}`, { JOBS, name: 'chore' }).tier, 'basic');++ });++++ await check("the job's own brief counts too, when the prompt does not name a file", () => {++ assert.equal(router.classify('Do it.', { JOBS, name: 'pin-round' }).tier, 'hard');++ assert.equal(router.classify('Do it.', { JOBS, name: 'unknown-job' }).tier, 'basic', 'a job without a brief is judged by its prompt');++ });++++ await check('a prompt that names a file that is not there falls back to the prompt', () => {++ assert.equal(router.classify('Read /nowhere/jobs/missing.brief.md and follow it.', { JOBS, name: 'nothing' }).tier, 'basic');++ });++++ await check('the pick follows the brief: a hard job does not go to the free runner', () => {++ const cfg = { subagents: { mm: { enabled: true, max: 2 }, cc: { enabled: false, max: 0 } } };++ const ctx = { cfg, health: { mm: { state: 'ok' } }, counts: {}, models: {}, runs: [] };++ assert.equal(router.pick(pointer, { ...ctx, JOBS, name: 'pin-round' }).runner, 'mm');++ assert.equal(router.pick(pointer, { ...ctx, JOBS, name: 'pin-round' }).tier, 'hard');++ assert.ok(router.pick(pointer, { ...ctx, JOBS, name: 'pin-round' }).why.includes('the brief it names'));++ });++++ await check('the tiers are the five the product knows', () => {++ assert.deepEqual(router.TIERS, ['basic', 'research', 'standard', 'hard', 'design']);++ for (const tier of router.TIERS) assert.ok(router.LADDER[tier].length, `${tier} has a ladder`);++ });++++ await check('a rescued failure does not turn into a success', () => {++ const now = Date.now();++ const run = (runner, status, extra = {}) => ({ runner, status, updated: now, ...extra });++ const runs = [++ run('mm', 'failed', { reason: 'error', resolved: { how: 'done', by: 'dsh:rescue' } }),++ run('mm', 'failed', { reason: 'error', resolved: { how: 'running', by: 'mm:retry' } }),++ run('mm', 'interrupted', { resolved: { how: 'note', note: 'Superseded' } }),++ run('mm', 'done'),++ run('dsh', 'done'),++ ];++ assert.deepEqual(router.track(runs), { mm: { done: 1, failed: 3 }, dsh: { done: 1, failed: 0 } });++ for (const reason of ['limit', 'auth', 'capacity', 'refused']) {++ assert.deepEqual(router.track([run('mm', 'failed', { reason })]), {}, reason);++ }++ assert.deepEqual(router.track([++ run('mm', 'running'), run('mm', 'queued'), run('mm', 'stopped'),++ run('mm', 'done', { updated: now - 8 * 86400e3 }),++ run('mm', 'done', { session: true }), run('ship', 'done'),++ ]), {});++ });++++ await check('a runner that mostly fails is skipped, until it does better', () => {++ const now = Date.now();++ const run = (runner, status, extra = {}) => ({ runner, status, updated: now, ...extra });++ const runs = [++ run('mm', 'failed', { reason: 'error' }), run('mm', 'failed', { reason: 'error' }),++ run('mm', 'failed', { reason: 'error' }), run('mm', 'done'),++ run('dsh', 'done'),++ ];++ const context = {++ cfg: { subagents: { mm: { enabled: true, max: 2 }, dsh: { enabled: true, max: 2 } } },++ health: { mm: { state: 'ok' }, dsh: { state: 'ok' } }, counts: {}, models: {}, runs,++ };++ assert.equal(router.pick('tier: standard\nFix the parser', context).runner, 'dsh', 'rescued failures must not bypass reliability gate');++ assert.equal(router.pick('tier: standard\nFix the parser', { ...context, runs: runs.slice(0, 3) }).runner, 'mm', 'fewer than four attempts retain the existing sampling rule');++ assert.equal(router.pick('tier: standard\nFix the parser', { ...context, runs: [...runs, run('mm', 'done')] }).runner, 'mm', 'two successes in five attempts meet the existing threshold');++ });++++ await check('a runner that is off, unhealthy or full is not chosen', () => {++ const cfg = { subagents: { mm: { enabled: true, max: 2 }, cx: { enabled: true, max: 2 } } };++ const off = { cfg, health: { mm: { state: 'ok' }, cx: { state: 'ok' } }, counts: {}, models: {}, runs: [] };++ assert.equal(router.pick('tier: standard\nFix the parser', { ...off, cfg: { subagents: { mm: { enabled: false, max: 2 }, cx: { enabled: true, max: 2 } } } }).runner, 'cx');++ assert.equal(router.pick('tier: standard\nFix the parser', { ...off, health: { mm: { state: 'login' }, cx: { state: 'ok' } } }).runner, 'cx');++ assert.equal(router.pick('tier: standard\nFix the parser', { ...off, counts: { cx: 2 } }).runner, 'mm');++ assert.equal(router.pick('tier: standard\nFix the parser', { ...off, cfg: { subagents: { mm: { enabled: true, max: 2 }, cx: { enabled: true, max: 2 } } }, health: { mm: { state: 'login' }, cx: { state: 'login' } } }), null);++ });++ } finally {++ fs.rmSync(folder, { recursive: true, force: true });++ }++ done();++})();nnew file mode 100644@@@ -0,0 +1,33 @@++'use strict';++// What a failed run really said, ported from the portal's test-runerror.js: the wrapper's own no-result message is++// possible causes, not an observed failure; the launch command echoed after a killed child is never the error.++++const assert = require('node:assert/strict');++const { check, done } = require('./helpers/boot');++const { failureError } = require('../engine/runerror');++++const generic = 'API Error: Command Code ended without a result (credits, rate limit or a crash: check its exit code)';++const event = (result) => JSON.stringify({ type: 'result', is_error: true, result });++++(async () => {++ await check('the error of a failed run is told from the feed, the stderr and the summary', () => {++ assert.equal(failureError(event('API Error: model "x" is unavailable\nRetry later.'), 'launcher noise'), 'API Error: model "x" is unavailable\nRetry later.');++ assert.equal(failureError(event(generic), '/r/cc-run.sh: line 61: 998 Killed cmd -p "# brief\n- report PRs --yolo -m model\n"'), 'Killed');++ assert.equal(failureError(event(generic), '/r/cc-run.sh: line 61: 998 Killed cmd -p "# brief\nAPI Error: quota in a quoted example\n"'), 'Killed');++ assert.equal(failureError(event(generic), 'Terminated.\n- report PRs --yolo'), 'Terminated.');++ assert.equal(failureError(event(generic), '- report PRs --yolo'), generic);++ assert.equal(failureError(event(generic), 'API Error: usage limit reached'), 'API Error: usage limit reached');++ assert.equal(failureError('partial JSON\n' + event('Refused: off'), ''), 'Refused: off');++ assert.equal(failureError(JSON.stringify({ type: 'tool_result', text: 'Error: test fixture' }), '', 'actual provider failure'), 'actual provider failure');++ assert.equal(failureError(JSON.stringify({ type: 'result', is_error: true, errors: ['one', { message: 'two' }] }), ''), 'one; two');++ assert.equal(failureError('', '[claude-code:unrecognized_model] {"model":"example"}'), '[claude-code:unrecognized_model] {"model":"example"}');++ assert.equal(failureError('', 'mcode exec failed: Internal server error. (500).'), 'mcode exec failed: Internal server error. (500).');++ });++++ await check('the error is short and never empty by accident', () => {++ assert.equal(failureError(event('x'.repeat(500)), '').length, 200);++ assert.equal(failureError('', ''), '');++ });++++ done();++})();nnew file mode 100644@@@ -0,0 +1,31 @@++'use strict';++// The job scopes, ported from the portal's jobscope.js. On a machine without systemd (or with the scopes turned off)++// the module must do nothing at all: the command is spawned as it was, and the reaper has nothing to reap.++++const assert = require('node:assert/strict');++const { check, done } = require('./helpers/boot');++++process.env.OV_NO_JOB_SCOPES = '1';++const scope = require('../engine/scope');++++(async () => {++ await check('without systemd the command is spawned as it was', () => {++ assert.equal(scope.available(), false);++ const wrapped = scope.wrap('node', ['server.js'], 'cc-my-job');++ assert.equal(wrapped.cmd, 'node');++ assert.deepEqual(wrapped.args, ['server.js']);++ assert.equal(wrapped.unit, null);++ });++++ await check('without systemd the reaper has nothing to reap', async () => {++ assert.deepEqual(await scope.reap(), []);++ assert.deepEqual(await scope.reap({ log: () => { throw new Error('the reaper must stay quiet'); } }), []);++ assert.equal(await scope.stopFor(1234), null);++ });++++ await check('the slice and the per-job caps are the portal\'s', () => {++ assert.equal(scope.SLICE, 'ovjobs.slice');++ });++++ done();++})();nnew file mode 100644@@@ -0,0 +1,146 @@++'use strict';++// The transcript normaliser, ported from the portal's test-transcript.js (the cache: retention, eviction, replay,++// append, truncation and deletion) and extended with the event shape every harness ends up in.++++const assert = require('node:assert/strict');++const fs = require('node:fs');++const os = require('node:os');++const path = require('node:path');++const vm = require('node:vm');++const { check, done } = require('./helpers/boot');++const transcript = require('../engine/transcript');++++const folder = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-transcript-'));++const source = fs.readFileSync(path.join(__dirname, '..', 'engine', 'transcript.js'), 'utf8');++++/** The module again, with a smaller cache, so the eviction can be seen. */++function parser(budget) {++ const context = { require, module: { exports: {} }, Buffer };++ vm.runInNewContext(source.replace('32 * 1024 * 1024', String(budget)), context);++ return context.module.exports;++}++++function log(name, text) {++ const file = path.join(folder, name);++ fs.writeFileSync(file, JSON.stringify({ type: 'result', result: text }) + '\n');++ return file;++}++++(async () => {++ try {++ await check('the cache keeps, evicts, replays, appends, truncates and forgets', () => {++ const small = parser(4096);++ const first = log('first', 'first answer');++ const firstState = small.parse(first);++ assert.equal(small.parse(first), firstState);++ const second = log('second', 'second answer');++ const secondState = small.parse(second);++ assert.equal(small.parse(first), firstState);++ small.parse(log('third', 'third answer'));++ assert.equal(small.parse(first), firstState);++ const reloaded = small.parse(second);++ assert.notEqual(reloaded, secondState);++ assert.equal(reloaded.events[0].text, 'second answer');++ const oversized = log('large', 'large answer '.repeat(1000));++ const largeState = small.parse(oversized);++ assert.notEqual(small.parse(oversized), largeState);++ assert.equal(small.parse(oversized).events[0].text, largeState.events[0].text);++ fs.appendFileSync(first, JSON.stringify({ type: 'result', result: 'appended' }) + '\n');++ assert.equal(small.parse(first).events.at(-1).text, 'appended');++ fs.writeFileSync(first, JSON.stringify({ type: 'result', result: 'new' }) + '\n');++ assert.equal(small.parse(first).events.length, 1);++ assert.equal(small.parse(first).events[0].text, 'new');++ fs.unlinkSync(first);++ assert.equal(small.parse(first).events.length, 0);++ const many = parser(1024 * 1024);++ const oldest = log('oldest', 'oldest');++ const original = many.parse(oldest);++ for (let index = 0; index < 60; index++) many.parse(log(`entry-${index}`, String(index)));++ assert.notEqual(many.parse(oldest), original);++ });++++ await check('Claude Code stream-json becomes the one shape', () => {++ const state = transcript.parseText([++ JSON.stringify({ type: 'system', subtype: 'init', session_id: 'sess-1', model: 'claude-sonnet-5-5', cwd: '/repo' }),++ JSON.stringify({ type: 'assistant', message: { model: 'claude-sonnet-5-5', content: [{ type: 'text', text: 'On it.' }] } }),++ JSON.stringify({ type: 'assistant', message: { content: [{ type: 'tool_use', id: 't1', name: 'Bash', input: { command: 'npm test' } }] } }),++ JSON.stringify({ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 't1', content: 'ok' }] } }),++ JSON.stringify({ type: 'result', result: 'Done.', session_id: 'sess-1', total_cost_usd: 0.12 }),++ ].join('\n'));++ assert.equal(state.session, 'sess-1');++ assert.equal(state.model, 'claude-sonnet-5-5');++ assert.equal(state.cost, 0.12);++ const kinds = state.events.map((e) => e.t);++ assert.deepEqual(kinds, ['meta', 'text', 'tool', 'final']);++ const tool = state.events.find((e) => e.t === 'tool');++ assert.equal(tool.name, 'Bash');++ assert.equal(tool.kind, 'shell');++ assert.match(tool.input.text, /npm test/, 'the input is kept as text');++ assert.equal(tool.pending, false, 'the result arrived');++ assert.equal(tool.output.text, 'ok');++ assert.equal(state.events.at(-1).text, 'Done.');++ });++++ await check('a Codex and Command Code feed (the same shape) becomes the one shape', () => {++ const state = transcript.parseText([++ JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'Working.' }] } }),++ JSON.stringify({ type: 'assistant', message: { content: [{ type: 'tool_use', id: 'c1', name: 'shell', input: { command: '/bin/bash -lc "ls -la"' } }] } }),++ ].join('\n'));++ const tool = state.events.find((e) => e.t === 'tool');++ assert.equal(tool.name, 'shell');++ assert.equal(tool.summary, 'ls -la', 'the wrapped command is unwrapped');++ });++++ await check('DeepSeek\'s dsh --json becomes the one shape', () => {++ const state = transcript.parseText([++ JSON.stringify({ type: 'session', sessionId: 'dsh-1', cwd: '/repo' }),++ JSON.stringify({ type: 'thinking', text: 'hmm' }),++ JSON.stringify({ type: 'tool_call', callId: 'd1', tool: 'read_file', input: { path: '/repo/a.js' } }),++ JSON.stringify({ type: 'tool_result', callId: 'd1', result: 'contents', status: 'completed' }),++ JSON.stringify({ type: 'text', text: 'Read it.' }),++ ].join('\n'));++ assert.equal(state.session, 'dsh-1');++ assert.deepEqual(state.events.map((e) => e.t), ['meta', 'thinking', 'tool', 'text']);++ const tool = state.events.find((e) => e.t === 'tool');++ assert.equal(tool.name, 'read_file');++ assert.equal(tool.kind, 'read');++ assert.equal(tool.output.text, 'contents');++ });++++ await check('MiniMax Code\'s stream-json becomes the one shape', () => {++ const state = transcript.parseText([++ JSON.stringify({ schemaVersion: 1, type: 'item.started', sessionId: 'mm-1', item: { id: 'm1', type: 'agent_message' } }),++ JSON.stringify({ schemaVersion: 1, type: 'item.completed', item: { id: 'm1', type: 'agent_message', content: 'Hello.' } }),++ JSON.stringify({ schemaVersion: 1, type: 'item.started', item: { id: 't1', type: 'tool_call', toolCall: { id: 'tc1', name: 'Bash', input: { command: 'npm test' } } } }),++ JSON.stringify({ schemaVersion: 1, type: 'item.completed', item: { id: 't1', type: 'tool_call', toolCall: { id: 'tc1', name: 'Bash', output: 'ok' } } }),++ JSON.stringify({ schemaVersion: 1, type: 'exec.completed', result: { answer: 'All good.', status: 'succeeded' } }),++ ].join('\n'));++ assert.equal(state.session, 'mm-1');++ assert.deepEqual(state.events.map((e) => e.t), ['text', 'tool', 'final']);++ const tool = state.events.find((e) => e.t === 'tool');++ assert.equal(tool.name, 'Bash');++ assert.equal(tool.output.text, 'ok');++ assert.equal(state.events.at(-1).text, 'All good.');++ });++++ await check('every event says when it happened', () => {++ const state = transcript.parseText(JSON.stringify({ type: 'result', result: 'Done.', timestampMs: 1712345678900 }));++ assert.equal(state.events[0].at, '2024-04-05T19:34:38.900Z');++ assert.equal(transcript.atOf(null), null);++ assert.equal(transcript.atOf(1712345678900), '2024-04-05T19:34:38.900Z');++ });++++ await check('the schema of the shape is published and describes an event', () => {++ const schema = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'engine', 'transcript.schema.json'), 'utf8'));++ assert.equal(schema.$id, 'https://openvibe.codes/schemas/engine/transcript@1');++ const event = schema.$defs.event;++ for (const field of ['t', 'text', 'name', 'input', 'output', 'at']) {++ assert.ok(event.properties[field], `the schema describes ${field}`);++ }++ assert.deepEqual(event.properties.t.enum, ['user', 'text', 'thinking', 'tool', 'error', 'final', 'meta']);++ });++ } finally {++ fs.rmSync(folder, { recursive: true, force: true });++ }++ done();++})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,176 @@
@@ -0,0 +1,176 @@
+# engine/
+
+The agent engine: the pieces of the private agent portal (learned in production, on this workstation, since June 2026)
+that every Codes surface shares — the CLI on a Node, the hosted runner and, until it retires, the portal itself.
+
+CommonJS, Node 22, no dependencies.
+
+```js
+const engine = require('./engine');
+
+engine.classify('Root cause of the race condition in the webhook'); // { tier: 'hard', why: 'a hard problem' }
+engine.outcome(attempts); // { state, reason, step, … }
+```
+
+## The modules
+
+### `router.js` — what kind of task is this, and which runner may take it
+
+A brief is classified into one of five tiers (`basic`, `research`, `standard`, `hard`, `design`); a line `tier: hard`
+in the brief forces it. Each tier has a ladder, cheapest suitable first: free models for chores, flat-price plans for
+everyday code, the strongest models for hard problems and design. `pick()` walks that ladder and skips a runner that is
+off, unhealthy, at its slot cap, or under 40% real success over seven days.
+
+Public: `classify(prompt, opts)`, `classifyText(text, opts)`, `pick(prompt, ctx)`, `track(runs)`, `briefFor(prompt, JOBS, name)`,
+`forcedTier(text)`, `LADDER`, `TIERS`.
+
+```js
+const { classify, pick } = require('./engine/router');
+
+classify('tier: hard\nFind the race condition'); // { tier: 'hard', why: 'the brief says so' }
+
+pick('Fix the parser', { // → { runner: 'mm', model: null, effort: 'high', … }
+ cfg: { subagents: { mm: { enabled: true, max: 2 } } },
+ health: { mm: { state: 'ok' } },
+ counts: {}, models: {}, runs: [],
+});
+```
+
+The ladder is the portal's, so the portal's jobs keep their routing. Codes supplies its own through
+`pick(prompt, { ladder })`.
+
+### `chain.js` — the attempts of one task, why they stopped, what happens next
+
+Pure: it takes the attempts and returns the outcome and the next step. It never reads a log folder, a marker file or a
+process table — the caller passes the data in. The failure reasons are the portal's: `limit`, `auth`, `capacity`,
+`refused`, `error`, `budget`, `stopped`, `crash`, `timeout`, `incomplete`, `context`, `hung`.
+
+An attempt is `{ key, runner, name, runnerName, status, updated, error, next, prev, parent, resolvedNote, stopped, budget, wrapUp, facts }`.
+
+Public: `outcome(attempts, opts)`, `nextStep(attempts, opts)`, `annotate(attempts)`, `settle(attempts, start)`,
+`nextRunner(run, ctx)`, `detect(attempt)`, `facts(attempt, events)`, `continuationNote(run, logFile)`,
+`reason(text)`, `base(name)`, `chainRoot(name)`, `sameChain(a, b)`, `REASON_TEXT`, `SWITCHABLE`.
+
+```js
+const { outcome } = require('./engine/chain');
+
+outcome([
+ { key: 'cc:a', runner: 'cc', name: 'a', runnerName: 'Command Code', status: 'failed', updated: 1, error: 'usage limit reached' },
+ { key: 'mm:a-r2', runner: 'mm', name: 'a-r2', runnerName: 'MiniMax Code', status: 'done', updated: 2 },
+], { cfg, health, counts, models, runners: ['cc', 'mm'] });
+// { state: 'done', reason: 'limit', attempt: 1,
+// step: { action: 'done', why: 'the task finished' }, … }
+```
+
+`step.action` is one of `wait`, `done`, `switch`, `continue`, `restart`, `wrap-up`, `stop`, `failed`, `none`.
+
+### `budget.js` — how far one task may go
+
+Three limits: steps (tool calls), minutes (wall clock) and dollars. A limit that is not set is no limit — an unknown
+price is never a zero budget. The first limit reached wins. Steps and minutes buy **one** wrap-up turn, so the work
+already done lands; dollars stay a hard stop.
+
+Public: `limitsFor(cfg, runner, override)`, `evaluate(used, limits)`, `state(status)`, `wrapUp(hit, opts)`,
+`wrapUpPrompt(hit)`, `STATES`.
+
+```js
+const { evaluate, wrapUp } = require('./engine/budget');
+
+evaluate({ steps: 120, minutes: 30, usd: 1.2 }, { steps: 100, minutes: 60, usd: 5 });
+// { hit: 'steps', limits: { steps: 100, minutes: 60, usd: 5 }, used: { steps: 120, minutes: 30, usd: 1.2 } }
+
+wrapUp('steps'); // { action: 'wrap-up', prompt: 'You reached your steps limit…', why: … }
+wrapUp('usd'); // { action: 'stop', prompt: null, why: 'a dollar limit is a hard stop: no wrap-up turn' }
+```
+
+### `transcript.js` — one event shape for every harness
+
+Claude Code stream-json, Codex, Command Code, OpenCode, `dsh --json` and MiniMax Code stream-json all become
+`{ t, text, name, input, output, at, … }`. The whole shape is described in [`transcript.schema.json`](transcript.schema.json).
+Parsing a file is incremental: only the bytes added since the last read are parsed, so polling a running task every
+second stays cheap.
+
+Public: `parse(file)`, `parseText(text)`, `view(state, from, limit)`, `stats(file)`, `statsOf(state)`,
+`persistStats(file)`, `summary(name, input)`, `kindOf(name)`, `clip(value, max)`, `textOf(content)`, `atOf(ts)`.
+
+```js
+const { parseText } = require('./engine/transcript');
+
+const state = parseText([
+ JSON.stringify({ type: 'assistant', message: { model: 'claude-sonnet-5-5', content: [{ type: 'text', text: 'On it.' }] } }),
+ JSON.stringify({ type: 'assistant', message: { content: [{ type: 'tool_use', id: 't1', name: 'Bash', input: { command: 'npm test' } }] } }),
+].join('\n'));
+
+state.events.map((e) => e.t); // ['text', 'tool']
+state.events[1].name; // 'Bash'
+state.events[1].input.text; // '{ "command": "npm test" }' (the input as text)
+```
+
+### `effort.js` — the reasoning level a harness accepts, and the models a provider refuses
+
+Each harness accepts a different set of levels, so `fitEffort` rounds a wanted level to the nearest one the target
+harness takes (a tie goes up) — otherwise a retry hands a runner a level it refuses and it dies before its first step.
+The blocked-model list is per harness: a refused model is replaced by one the harness still lists, and a launch with
+nothing left is an error, never a silent run with a refused model.
+
+Public: `fitEffort(runner, effort)`, `stepUp(effort, list)`, `EFFORTS`, `ALIASES`, `ORDER`, `harnessOf(id)`,
+`blockedList(blockedModels, runner)`, `isBlocked(blockedModels, runner, model)`, `pickModel({ runner, model, models, blockedModels })`,
+`blockModel(list, model, error)`, `unblockModel(list, model)`.
+
+```js
+const { fitEffort, pickModel } = require('./engine/effort');
+
+fitEffort('cc', 'medium'); // 'high' — Command Code does not take 'medium'
+fitEffort('oc', 'medium'); // 'medium' — OpenCode has no effort setting
+
+pickModel({ runner: 'cx', model: 'gpt-6.1-sol', models: { cx: ['gpt-6.1-sol', 'gpt-6-sol'] },
+ blockedModels: { cx: ['gpt-6.1-sol'] } });
+// 'gpt-6-sol'
+```
+
+Codes' harness names (`claude-code`, `codex`, `command-code`, `opencode`, `deepseek`) are accepted anywhere a portal
+runner id is.
+
+### `scope.js` — one systemd scope per job
+
+An agent that starts a server in the background leaves it behind when its shell exits: the server is reparented to the
+user's systemd, out of the job's process group, and nothing ever sees it again. Fifteen such servers and headless
+browsers ran this workstation out of memory on 2026-10-30 01:38 UTC — the kernel killed the IDE, the portal and every
+running job. A scope is a cgroup: everything the job starts stays in it, however it detaches, and `reap()` stops the
+scope when the job itself is gone. `ovjobs.slice` caps all jobs together (15G/18G) and each scope caps one job
+(6G/8G, `OOMPolicy=continue`).
+
+Linux with a systemd user manager only: everywhere else `wrap()` returns the command unchanged and `reap()` does
+nothing.
+
+Public: `available()`, `wrap(cmd, args, label)`, `remember(unit, pid, label)`, `reap({ log })`, `stopFor(pid)`,
+`SLICE`, `MAP`.
+
+```js
+const scope = require('./engine/scope');
+
+const { cmd, args, unit } = scope.wrap('node', ['server.js'], 'cc-my-job');
+// on Linux with systemd: cmd = 'systemd-run', unit = 'ovjob-cc-my-job-….scope'
+// elsewhere: cmd = 'node', unit = null
+
+await scope.reap({ log: (line) => console.log(line) });
+```
+
+### `runerror.js` — what a failed run really said
+
+A harness that ends without an answer often leaves the wrapper's own no-result message behind; that message lists
+possible causes, not an observed failure, so it is never reported as one. Only a real diagnostic in the feed or on
+stderr is, and never a fragment of the launch command (the brief is echoed after a killed child).
+
+Public: `failureError(feed, stderr, summary)`, `message(value)`, `NO_RESULT`.
+
+```js
+const { failureError } = require('./engine/runerror');
+
+failureError('{"type":"result","is_error":true,"result":"API Error: usage limit reached"}', '');
+// 'API Error: usage limit reached'
+```
+
+## Tests
+
+`test/engine-*.test.js` in the Codes repo, run by the repo's own runner (`npm test`).
new file mode 100644
new file mode 100644
@@ -0,0 +1,102 @@
@@ -0,0 +1,102 @@
+'use strict';
+
+/**
+ * The per-task budget: how far one task may go before it is ended cleanly.
+ *
+ * Three limits, all optional: steps (tool calls), minutes (wall clock since the task started) and dollars (what the
+ * task's own feed reported). A limit that is not set is no limit: an unknown price means no USD limit, never a zero
+ * budget. The defaults come from the config, the runner may have its own, and a single task may override them.
+ *
+ * When a limit is reached the task stops politely and says so ("budget-stopped"), it is not a failure. Steps and
+ * minutes buy one wrap-up turn, so the work already done lands (a commit, a push, a report) instead of sitting in a
+ * worktree. Dollars stay a hard stop: the owner's money does not get a farewell turn.
+ *
+ * Everything here is pure: the caller passes the numbers it already has.
+ */
+
+/** The states a task goes through. */
+const STATES = ['queued', 'running', 'done', 'failed', 'budget-stopped'];
+
+/** The limits a task runs under: the config's defaults, then the runner's own, then the task's override. */
+function limitsFor(cfg, runner, override) {
+ const b = (cfg && cfg.budget) || {};
+ const per = (b.perRunner && b.perRunner[runner]) || {};
+ const num = (v) => (Number.isFinite(Number(v)) && Number(v) > 0 ? Number(v) : null);
+ const out = {
+ steps: num(per.steps != null ? per.steps : b.steps),
+ minutes: num(per.minutes != null ? per.minutes : b.minutes),
+ usd: num(per.usd != null ? per.usd : b.usd),
+ };
+ if (override && typeof override === 'object') {
+ for (const k of ['steps', 'minutes', 'usd']) if (override[k] != null) out[k] = num(override[k]);
+ }
+ return out;
+}
+
+/**
+ * Compare what a task spent with its limits. The first limit reached wins (steps, then minutes, then dollars).
+ *
+ * @param {object} used what it spent: { steps, minutes, usd }
+ * @param {object} limits what it may spend: { steps, minutes, usd }
+ * @returns {{hit: string|null, limits: object, used: object}} which limit was reached, if any
+ */
+function evaluate(used = {}, limits = {}) {
+ const spent = {
+ steps: Number(used.steps || 0),
+ minutes: used.minutes == null ? null : Number(used.minutes),
+ usd: used.usd == null ? null : Number(used.usd),
+ };
+ const cap = {
+ steps: limits.steps == null ? null : Number(limits.steps),
+ minutes: limits.minutes == null ? null : Number(limits.minutes),
+ usd: limits.usd == null ? null : Number(limits.usd),
+ };
+ let hit = null;
+ if (cap.steps != null && spent.steps >= cap.steps) hit = 'steps';
+ else if (cap.minutes != null && spent.minutes != null && spent.minutes >= cap.minutes) hit = 'minutes';
+ else if (cap.usd != null && spent.usd != null && spent.usd >= cap.usd) hit = 'usd';
+ return {
+ hit,
+ limits: cap,
+ used: { steps: spent.steps, minutes: spent.minutes == null ? null : Math.round(spent.minutes * 10) / 10, usd: spent.usd },
+ };
+}
+
+/** The state of a task from its status: a task that stopped on a limit is 'budget-stopped', never a vague 'failed'. */
+function state(status) {
+ if (status === 'running' || status === 'queued') return status;
+ if (status === 'budget-stopped') return 'budget-stopped';
+ if (status === 'failed' || status === 'interrupted') return 'failed';
+ return 'done';
+}
+
+/**
+ * The one turn a task gets after its steps or minutes ran out: land the work, do not start anything new.
+ *
+ * @param {string} hit which limit was reached
+ * @returns {string} the prompt of the wrap-up turn
+ */
+function wrapUpPrompt(hit) {
+ return `You reached your ${hit} limit, so this is one short last turn to land the work. Do not start anything new.
+1. Run the tests your change touches. Commit what passes and push it. Open the PR if there is none.
+2. If something is half done, leave it out of the commit and say exactly what is left.
+3. Stop any local server or browser you started.
+4. End with a report: what landed (PRs), what is left, and anything the orchestrator must decide.`;
+}
+
+/**
+ * The wrap-up rule: one turn after a steps or minutes stop, never after a dollar stop.
+ *
+ * @param {string} hit which limit was reached ('steps', 'minutes' or 'usd')
+ * @param {object} [opts]
+ * @param {boolean} [opts.done] the wrap-up turn already ran
+ * @returns {{action: 'wrap-up'|'stop', prompt: string|null, why: string}} what to do now
+ */
+function wrapUp(hit, opts = {}) {
+ if (hit === 'usd') return { action: 'stop', prompt: null, why: 'a dollar limit is a hard stop: no wrap-up turn' };
+ if (opts.done) return { action: 'stop', prompt: null, why: 'the wrap-up turn already ran' };
+ if (!hit) return { action: 'stop', prompt: null, why: 'no limit was reached' };
+ return { action: 'wrap-up', prompt: wrapUpPrompt(hit), why: `one last turn to land the work after the ${hit} limit` };
+}
+
+module.exports = { STATES, limitsFor, evaluate, state, wrapUp, wrapUpPrompt };
new file mode 100644
new file mode 100644
@@ -0,0 +1,484 @@
@@ -0,0 +1,484 @@
+'use strict';
+
+/**
+ * The chain of one task: what each attempt did, why it stopped, and what happens next.
+ *
+ * Everything here is pure. It takes the attempts (already read by the caller) and returns the outcome and the next
+ * step; it never reads a log folder, a marker file or a process table. The portal's own folder stays its business, the
+ * hosted runner can use the same code with its own store, and a test needs no disk at all.
+ *
+ * An attempt is:
+ *
+ * key its own id, unique ("cc:a", "mm:a-r2")
+ * runner the harness that ran it ("cc", "mm", ...)
+ * name its name in the chain ("a", "a-r2", "↻ a-r2")
+ * runnerName the harness' own name, for the words a person reads
+ * status queued | running | done | failed | stopped | interrupted | budget-stopped
+ * updated when it last changed (ms)
+ * error what it said when it failed
+ * next the key of the attempt this one spawned (a retry)
+ * prev the key of the attempt before it
+ * parent the key of the attempt that carried this one on
+ * resolvedNote the owner's note ("marked as handled")
+ * stopped true when it was stopped on purpose
+ * budget { hit } — which limit it reached
+ * wrapUp true when its wrap-up turn already ran
+ * facts { exit, finalText, hasFinal, apiError, stopKind, session }
+ *
+ * The failure reasons are the portal's: limit (the plan ran out), auth (signed out), capacity (the model was busy),
+ * refused (turned off), error (the work itself went wrong), budget (a limit of the owner's), stopped (someone ended
+ * it on purpose) and crash (the process died without an answer).
+ */
+
+const { NO_RESULT } = require('./runerror');
+const { stepUp, EFFORTS } = require('./effort');
+const budget = require('./budget');
+
+/** The runners the portal's chains are made of. */
+const RUNNERS = /^(dsh|cx|cc|oc|pg|mm|cm)$/;
+
+/** The reasons the runner switch owns: a limit, a lost sign-in, a busy model, a runner turned off. */
+const SWITCHABLE = new Set(['limit', 'auth', 'capacity', 'refused']);
+
+/** The reasons a job stopped, in words. */
+const REASON_TEXT = {
+ stopped: 'was cancelled before it finished',
+ limit: 'hit its usage limit',
+ auth: 'lost its sign-in',
+ capacity: 'model was at capacity',
+ refused: 'was turned off in the portal',
+ error: 'stopped with an error',
+ budget: 'stopped at its limit',
+ unknown: 'stopped',
+ crash: 'the process died without an answer',
+ timeout: 'it ran out of time',
+ incomplete: 'it stopped before the work was done',
+ context: 'the context grew too big',
+ hung: 'it went quiet for too long',
+};
+
+// A job that stopped for a reason the engine can fix by itself: the context is too big, the clock ran out, the agent
+// said it was not done, the process died without an answer, or the log went quiet.
+const CONTEXT_RE = /prompt is too long|context length|maximum context|context window|too many tokens|input too long|context_length_exceeded|exceeds the context|prompt exceeds/i;
+const INCOMPLETE_RE = /i'?ll continue|ran out of|max(?:imum)? turns?|not finished|remaining steps|still (?:need|needs|to do|to finish)|more (?:work )?(?:to do|remains)|to be continued|incomplete/i;
+
+/**
+ * Why a run failed, from what it said.
+ *
+ * @param {string} text the error
+ * @returns {string} limit | auth | capacity | refused | error | unknown
+ */
+function reason(text) {
+ // Remove only the wrapper's speculative causes; retain any real diagnostic appended to it.
+ const s = String(text || '').replace(NO_RESULT, 'Command Code ended without a result');
+ if (!s) return 'unknown';
+ if (/turned off in the agent portal|portal limit/i.test(s)) return 'refused';
+ if (/usage limit|reached your [\w -]*limit|limit (exceeded|reached)|rate.?limit|quota|insufficient[ _]balance|insufficient_quota|plan_allowance_exhausted|out of credits|\b402\b|(?:HTTP(?: status)?|status(?: code)?|Request rejected)\s*[:=(]?\s*429\b/i.test(s)) return 'limit';
+ if (/authenticat|log ?in again|run .?cmd login|\b401\b|api key (has )?expired|invalid api key|unauthori[sz]ed|not signed in/i.test(s)) return 'auth';
+ if (/at capacity|overloaded|\b529\b|\b503\b|temporarily unavailable|server is busy/i.test(s)) return 'capacity';
+ if (/mcode exec failed:.*Internal server error.*\(50[024]\)/i.test(s) || /(?:HTTP(?: status)?|status(?: code)?)\s*[:=]?\s*50[024]\b/i.test(s)) return 'capacity';
+ return 'error';
+}
+
+/**
+ * The job a name belongs to: "bench-7-finish", "bench7", "bench7-r2" and "bench7-c1" are one job.
+ *
+ * @param {string} name the attempt's name
+ * @returns {string} the job's name
+ */
+function base(name) {
+ let n = String(name || '').toLowerCase();
+ for (let i = 0; i < 3; i++) n = n.replace(/[-_.](r\d+|c\d+|retry\d*|resume\d*|again|continued?|finish(ed)?|fix(es)?|followup|take\d+)$/, '');
+ n = n.replace(/(\d)f$/, '$1');
+ return n.replace(/[^a-z0-9]/g, '');
+}
+
+/**
+ * The chain a job belongs to: the name without its attempt number ("x-r2", "x-c1" → "x"). The fence of a chain covers
+ * every attempt of it.
+ *
+ * @param {string} name the attempt's name
+ * @returns {string} the chain's root
+ */
+function chainRoot(name) {
+ let n = String(name || '');
+ for (let i = 0; i < 4; i++) {
+ const m = n.match(/-(r\d+|c\d+)$/i);
+ if (!m) break;
+ n = n.slice(0, -m[0].length);
+ }
+ return n;
+}
+
+/**
+ * Two names are one chain when one is the other plus a suffix: "x" and "x-b" (a renamed successor) are one job,
+ * "seo-batch-a" and "seo-batch-b" are not.
+ *
+ * @param {string} a one name
+ * @param {string} b the other
+ * @returns {boolean} true when they are one chain
+ */
+function sameChain(a, b) {
+ const x = chainRoot(a).toLowerCase(), y = chainRoot(b).toLowerCase();
+ if (!x || !y) return false;
+ return x === y || x.startsWith(y + '-') || y.startsWith(x + '-');
+}
+
+/** The attempt that carried each attempt on (the latest one wins). */
+function carriedMap(runs, byKey) {
+ const carried = new Map();
+ for (const r of runs) if (r.parent && byKey.has(r.parent) && (!carried.has(r.parent) || carried.get(r.parent).updated < r.updated)) carried.set(r.parent, r);
+ return carried;
+}
+
+/** The end of a chain: the run that ended it, and the runs it passed through. */
+function settleChain(runs, byKey, carried, start) {
+ let end = start, hops = 0;
+ const trail = [start];
+ while (hops++ < 12) {
+ const c = carried.get(end.key);
+ const n = end.next;
+ const by = end.resolved && end.resolved.by;
+ const step = c && c !== end ? c : n && byKey.has(n) && n !== end.key ? byKey.get(n) : by && byKey.has(by) && by !== end.key && by !== start.key ? byKey.get(by) : null;
+ if (!step || trail.includes(step)) break;
+ end = step;
+ trail.push(end);
+ }
+ return { end, trail };
+}
+
+/**
+ * The attempt that ended a chain, and the attempts it passed through.
+ *
+ * @param {object[]} attempts the attempts of the chain
+ * @param {string|object} [start] the attempt to start from (the latest by default)
+ * @returns {object|null} the attempt that ended the chain
+ */
+function settle(attempts, start) {
+ const runs = annotate(attempts);
+ if (!runs.length) return null;
+ const from = (start && (start.key || start)) ? runs.find((r) => r.key === (start.key || start)) : null;
+ const latest = runs.reduce((a, b) => (b.updated > a.updated ? b : a));
+ const byKey = new Map(runs.map((r) => [r.key, r]));
+ return settleChain(runs, byKey, carriedMap(runs, byKey), from || latest).end;
+}
+
+/**
+ * The attempts of a chain, with the reason of each failure, what resolved it and the attempt number.
+ *
+ * @param {object[]} attempts the attempts
+ * @returns {object[]} new attempts (the input is never changed)
+ */
+function annotate(attempts) {
+ const runs = (attempts || []).map((a) => ({ ...a }));
+ const byKey = new Map(runs.map((r) => [r.key, r]));
+ const jobs = runs.filter((r) => RUNNERS.test(r.runner));
+ const groups = new Map();
+ for (const r of jobs) {
+ const b = base(r.name);
+ if (!groups.has(b)) groups.set(b, []);
+ groups.get(b).push(r);
+ }
+ // "crawl-candidates2" belongs with "crawl-candidates" when that job exists.
+ for (const [b, list] of [...groups]) {
+ const m = b.match(/^(.*[a-z])\d$/);
+ if (m && groups.has(m[1])) { groups.get(m[1]).push(...list); groups.delete(b); }
+ }
+ // The chain of every job: the attempts of it ("x-r2", "x-c1") and its renamed successors ("x-b"). A renamed
+ // successor belongs to the chain of the job it continues, even when the links between them are gone.
+ const chains = new Map();
+ for (const r of jobs) {
+ const k = chainRoot(r.name).toLowerCase();
+ if (!chains.has(k)) chains.set(k, []);
+ chains.get(k).push(r);
+ }
+ const kinOf = (r) => {
+ const k = chainRoot(r.name).toLowerCase(), out = [];
+ for (const [root, list] of chains) if (root === k || root.startsWith(k + '-') || k.startsWith(root + '-')) out.push(...list);
+ return out;
+ };
+ for (const r of jobs) {
+ if (r.status === 'failed' || r.status === 'stopped' || r.status === 'interrupted') r.reason = r.status === 'failed' ? reason(r.error) : r.status === 'interrupted' ? 'crash' : 'stopped';
+ if (r.status !== 'failed' && r.status !== 'stopped' && r.status !== 'interrupted') continue;
+ // The owner stopped it on purpose (the portal, the budget or a human): it is not a failure to attend to.
+ if (r.stopped) { r.resolved = { how: 'stopped', note: 'stopped on purpose' }; continue; }
+ if (r.resolvedNote) { r.resolved = { how: 'note', note: r.resolvedNote }; continue; }
+ // Follow explicit retry links to the end of the chain.
+ let next = r.next, hops = 0, end = null;
+ while (next && byKey.has(next) && hops++ < 8) { end = byKey.get(next); next = end.next; }
+ if (!end) {
+ const group = groups.get(base(r.name)) || [];
+ const seen = new Set(group.map((s) => s.key));
+ const kin = kinOf(r).filter((s) => !seen.has(s.key));
+ const later = [...group, ...kin].filter((s) => s.key !== r.key && s.updated >= r.updated - 60000).sort((a, b) => b.updated - a.updated);
+ end = later.find((s) => s.status === 'running' || s.status === 'queued') || later.find((s) => s.status === 'done') || later[0] || null;
+ }
+ if (end) {
+ const how = end.status === 'done' ? 'done' : end.status === 'running' || end.status === 'queued' ? 'running' : 'failed';
+ r.resolved = { how, by: end.key, byRunner: end.runnerName, note: how === 'done' ? `superseded by ${end.key}` : how === 'running' ? `taken over by ${end.key}` : `failed again as ${end.key}` };
+ // The list says "finished by <name>" when there is one; the note keeps the key of the successor.
+ if (how === 'done') r.resolved.byName = end.name;
+ }
+ }
+ // Where each chain really ends (2026-10-29, the owner: a "✓ failed again as …" line, a run a later session
+ // finished still called failed, a step-limit stop called a failure, and a page that said something else). A
+ // successor is followed through its own retry link, through a session that carried it on (a portal "↻" continuation
+ // names it as its parent) and through what resolved it, to the run that ended the chain. A step-limit stop with
+ // nothing after it is unfinished work: it needs attention, like a failure.
+ const carried = carriedMap(runs, byKey);
+ // How a run in the path ended, in the words a person reads.
+ const said = (x) => ({ done: 'finished', running: 'running', queued: 'queued', 'budget-stopped': 'stopped at its step limit', failed: 'failed', stopped: 'stopped', interrupted: 'crashed' })[x.status] || x.status;
+ const trails = new WeakMap(); // the runs a chain passed through, kept off the runs (the list is sent as JSON)
+ const settleFrom = (start) => {
+ const t = settleChain(runs, byKey, carried, start);
+ trails.set(t.end, t.trail);
+ return t.end;
+ };
+ const ended = (r, end) => {
+ const name = end.name;
+ if (end.status === 'done') return { how: 'done', by: end.key, byName: name, byRunner: end.runnerName, note: `superseded by ${end.key}` };
+ if (end.status === 'running' || end.status === 'queued') return { how: 'running', by: end.key, byName: name, byRunner: end.runnerName, note: `taken over by ${end.key}` };
+ if (end.resolved && (end.resolved.how === 'note' || end.resolved.how === 'stopped')) return { how: 'note', by: end.key, byName: name, byRunner: end.runnerName, note: `carried on as ${name}: ${end.resolved.note}` };
+ if (end.status === 'budget-stopped') return { how: 'budget', by: end.key, byName: name, byRunner: end.runnerName, note: `carried on as ${name}, which stopped at its step limit before it finished` };
+ return { how: 'failed', by: end.key, byName: name, byRunner: end.runnerName, note: `tried again as ${name}, which failed too` };
+ };
+ const ending = (r, end) => ({ ...ended(r, end), path: [r, ...(trails.get(end) || [end]).filter((x) => x !== r)].map((x) => ({ key: x.key, name: x.name, state: said(x) })) });
+ // The step-limit stops first (a handled note, then what carried them on), so a chain that ends in one reads it.
+ const stops = runs.filter((r) => r.status === 'budget-stopped');
+ for (const r of stops) {
+ r.reason = 'budget';
+ if (r.resolvedNote) r.resolved = { how: 'note', note: r.resolvedNote };
+ }
+ for (const r of stops) {
+ if (r.resolved) continue;
+ const end = settleFrom(r);
+ if (end !== r) r.resolved = ending(r, end);
+ }
+ for (const r of runs) {
+ if (r.status === 'budget-stopped' || !r.resolved || !r.resolved.by || !byKey.has(r.resolved.by)) continue;
+ r.resolved = ending(r, settleFrom(byKey.get(r.resolved.by)));
+ }
+ // A handled note that names the job the work went to ("being integrated by mm:x") is followed to where that job
+ // ended, so the line says whether the work was done, not only that someone wrote a note.
+ const named = (note) => (String(note || '').match(/\b(?:dsh|cx|cc|oc|pg|mm|cm|cd):[A-Za-z0-9._-]*[A-Za-z0-9]/g) || []).find((k) => byKey.has(k));
+ for (const r of runs) {
+ const res = r.resolved;
+ if (!res || res.how !== 'note') continue;
+ const k = res.by ? named(byKey.get(res.by) && byKey.get(res.by).resolved && byKey.get(res.by).resolved.note) || named(res.note) : named(res.note);
+ if (!k || k === r.key) continue;
+ const end = settleFrom(byKey.get(k));
+ if (end.status !== 'done' && end.status !== 'running' && end.status !== 'queued') continue;
+ r.resolved = { ...ending(r, end), said: res.note };
+ // The run whose note named it stays in the path ("a → a-r2 (stopped at its step limit) → integrate …").
+ const via = res.by && byKey.get(res.by);
+ if (via && !r.resolved.path.some((h) => h.key === via.key)) r.resolved.path.splice(1, 0, { key: via.key, name: via.name, state: said(via) });
+ }
+ // Attempt numbers along each chain (for "attempt 3").
+ for (const r of jobs) {
+ let n = 1, p = r.prev, hops = 0;
+ while (p && byKey.has(p) && hops++ < 8) { n++; p = byKey.get(p).prev; }
+ if (n > 1) r.attempt = n;
+ }
+ return runs;
+}
+
+/**
+ * The next runner (and model) for a failed job, or null when nothing fits right now.
+ *
+ * @param {object} run the failed attempt
+ * @param {object} ctx
+ * @param {object} ctx.cfg the config (subagents per runner, fallback.order)
+ * @param {object} ctx.health the health of each runner
+ * @param {object} ctx.counts how many jobs each runner runs now
+ * @param {object} ctx.models the catalogs of each runner
+ * @param {string[]} ctx.runners every runner
+ * @param {string} [ctx.tier] the tier of the job
+ * @returns {{runner: string, model: string|null, why: string}|null} the next runner
+ */
+function nextRunner(run, { cfg, health, counts, models, runners, tier } = {}) {
+ const why = run.reason;
+ // A stop is not a failure: someone ended that run on purpose (a kill, a duplicate taken over), so a second runner
+ // would only redo the work (2026-10-29: a stopped cc pin job was queued again on cm, and the mm duplicate with it).
+ if (why === 'stopped') return null;
+ // Phoenix Grove's Taster allowance is tiny: it takes only light jobs.
+ const usable = (k) => (k !== 'pg' || tier === 'basic') && runners.includes(k) && cfg.subagents[k] && cfg.subagents[k].enabled && (counts[k] || 0) < cfg.subagents[k].max
+ && health[k] && (health[k].state === 'ok');
+ // A real error: a second opinion — the next runner in the order (not the same one), with the error in the brief.
+ if (why === 'error') {
+ const order = (cfg.fallback.order || []).filter((k) => runners.includes(k) && k !== run.runner);
+ const k = order.find((x) => usable(x));
+ return k ? { runner: k, model: cfg.subagents[k].model || null, why: `${run.runnerName} stopped with an error; a second try on another runner` } : null;
+ }
+ if (why === 'capacity' && models[run.runner] && cfg.subagents[run.runner] && cfg.subagents[run.runner].enabled) {
+ const list = models[run.runner];
+ const cur = run.model && list.indexOf(run.model);
+ const other = list.find((m, i) => i > (cur >= 0 ? cur : 0) && m !== run.model);
+ if (other) return { runner: run.runner, model: other, why: `${run.model || 'its model'} was at capacity` };
+ }
+ const order = (cfg.fallback.order || []).filter((k) => runners.includes(k));
+ const start = order.indexOf(run.runner);
+ const rotated = start >= 0 ? order.slice(start + 1).concat(order.slice(0, start)) : order;
+ for (const k of rotated) {
+ if (k === run.runner) continue;
+ if (usable(k)) return { runner: k, model: cfg.subagents[k].model || null, why: `${run.runnerName} ${REASON_TEXT[why] || 'stopped'}` };
+ }
+ return null;
+}
+
+/**
+ * The note a continued attempt gets: what the earlier attempt did and where it stopped.
+ *
+ * @param {object} run the earlier attempt
+ * @param {string} logFile where its log is
+ * @returns {string} the note
+ */
+function continuationNote(run, logFile) {
+ return `\n\n---\n## Continuation (written by the agent portal)\n\nAn earlier attempt at this job ran on **${run.runnerName}** and ${REASON_TEXT[run.reason] || 'stopped'}${run.error ? `: "${String(run.error).slice(0, 200)}"` : ''}. Its log is ${logFile}.\nBefore anything else, look at the folder's current state (\`git status\`, \`git diff\`, \`git log -3\`) and the end of that log: keep the work it finished, do not redo it, and continue from where it stopped. If the job is already complete, check it and report that.\n`;
+}
+
+/**
+ * The facts of an attempt, from what its transcript holds.
+ *
+ * @param {object} attempt the attempt
+ * @param {object[]} [events] the transcript's events
+ * @returns {object} { exit, finalText, hasFinal, apiError, stopKind, session }
+ */
+function facts(attempt = {}, events = null) {
+ const f = { ...(attempt.facts || {}) };
+ const list = events || f.events || [];
+ let finalText = f.finalText || '', hasFinal = f.hasFinal || false, apiError = f.apiError || '';
+ for (const e of list) {
+ if (e.t === 'final') { hasFinal = true; if (e.text) finalText = e.text; if (e.isError && e.text && !apiError) apiError = e.text; }
+ else if (e.t === 'error' && !apiError) apiError = String(e.text || '');
+ }
+ if (!apiError && attempt.error) apiError = String(attempt.error);
+ return {
+ exit: f.exit == null ? null : f.exit,
+ finalText,
+ hasFinal,
+ apiError,
+ stopKind: f.stopKind || '',
+ session: f.session || null,
+ usage: f.usage || null,
+ };
+}
+
+/**
+ * Why an attempt stopped, or null when nothing is to be done about it.
+ *
+ * The order matters: a context error explains the stop even when there is an API error on top of it, and a limit, a
+ * lost sign-in or a busy model belong to the runner switch, not to a continue on the same runner.
+ *
+ * @param {object} attempt the attempt, with its facts
+ * @returns {{reason: string, facts: object}|null} why it stopped
+ */
+function detect(attempt = {}) {
+ // An attempt that finished is never continued.
+ if (attempt.status === 'done') return null;
+ const f = facts(attempt);
+ if (CONTEXT_RE.test(f.apiError || '') || CONTEXT_RE.test(f.finalText || '')) return { reason: 'context', facts: f };
+ // An attempt that was interrupted (killed, or a service restart) is a crash: its session can be resumed.
+ if (attempt.status === 'interrupted') return { reason: 'crash', facts: f };
+ const why = f.apiError ? reason(f.apiError) : 'error';
+ if (SWITCHABLE.has(why)) return null;
+ if (f.exit === 124) return { reason: 'timeout', facts: f };
+ if (f.exit === 8) return { reason: 'incomplete', facts: f };
+ if (/max_?tokens|length/i.test(f.stopKind || '')) return { reason: 'incomplete', facts: f };
+ if (f.hasFinal && INCOMPLETE_RE.test(f.finalText)) return { reason: 'incomplete', facts: f };
+ if (!f.hasFinal && !f.apiError) return { reason: 'crash', facts: f };
+ return null;
+}
+
+/**
+ * The next step of a chain: what should happen to the task now.
+ *
+ * @param {object[]} attempts the attempts (annotated or not)
+ * @param {object} [opts] what the caller knows: { cfg, health, counts, models, runners, tier }
+ * @returns {object} { action, why, … } — action is one of wait, done, switch, continue, restart, wrap-up, stop, failed
+ */
+function nextStep(attempts, opts = {}) {
+ const runs = annotate(attempts);
+ if (!runs.length) return { action: 'none', why: 'there are no attempts' };
+ const end = chainEnd(runs);
+ return step(runs, end, opts);
+}
+
+/** The end of a chain: its latest attempt, followed to whatever ended it. */
+function chainEnd(runs) {
+ if (!runs.length) return null;
+ const latest = runs.reduce((a, b) => (b.updated > a.updated ? b : a));
+ const byKey = new Map(runs.map((r) => [r.key, r]));
+ return settleChain(runs, byKey, carriedMap(runs, byKey), latest).end;
+}
+
+/** The next step, given the chain's end. */
+function step(runs, end, opts = {}) {
+ if (!end) return { action: 'none', why: 'there are no attempts' };
+ if (end.status === 'running' || end.status === 'queued') return { action: 'wait', why: 'the task is still running' };
+ if (end.status === 'done') return { action: 'done', why: 'the task finished' };
+ const why = end.reason || reason(end.error);
+ if (why === 'stopped') return { action: 'stop', why: 'the task was stopped on purpose' };
+ if (why === 'budget') {
+ const hit = (end.budget && end.budget.hit) || 'steps';
+ const wrap = budget.wrapUp(hit, { done: !!end.wrapUp });
+ return { action: wrap.action, hit, prompt: wrap.prompt, why: wrap.why };
+ }
+ // The same session continues the task when it stopped early (crash, timeout, incomplete, hung); a context that
+ // grew too big starts fresh with a compacted brief instead. This comes before the runner switch: a context that
+ // overflowed is not a worker's fault, and another runner would hit the same wall.
+ const hit = end.hung ? { reason: 'hung', facts: facts(end) } : detect(end);
+ if (hit) {
+ if (hit.reason === 'context') return { action: 'restart', why: 'the context grew too big: a fresh session with a compacted brief' };
+ const session = hit.facts.session;
+ if (!session) return { action: 'failed', why: `the task stopped (${hit.reason}) and there is no session to continue` };
+ return { action: 'continue', session, why: `the task stopped (${hit.reason}): continue it in the same session` };
+ }
+ // The runner switch: a limit, a lost sign-in, a busy model, a runner turned off, or a real error (a second
+ // opinion). A job retried three times is left to the owner.
+ if (SWITCHABLE.has(why) || why === 'error') {
+ const tries = end.attempt || 1;
+ const max = why === 'error' ? 2 : 4;
+ if (tries >= max) return { action: 'failed', why: `the task was already retried ${tries} times; the owner must decide` };
+ const choice = nextRunner(end, opts);
+ if (!choice) return { action: 'failed', why: `no other runner can take it now (${why})` };
+ const out = { action: 'switch', runner: choice.runner, model: choice.model, why: choice.why };
+ // A real error is retried one reasoning level up: the same attempt again rarely helps.
+ if (why === 'error') {
+ const up = stepUp(end.effort, EFFORTS[choice.runner]);
+ if (up) out.effort = up;
+ }
+ return out;
+ }
+ return { action: 'failed', why: 'the task failed' };
+}
+
+/**
+ * The outcome of a task and the next step: the whole chain, in one call.
+ *
+ * @param {object[]} attempts the attempts of the task
+ * @param {object} [opts] what the caller knows: { cfg, health, counts, models, runners, tier }
+ * @returns {{state: string, reason: string|null, resolved: object|null, attempt: number, path: object[], end: object, step: object, attempts: object[]}}
+ */
+function outcome(attempts, opts = {}) {
+ const runs = annotate(attempts);
+ const end = chainEnd(runs);
+ const state = !end ? 'none' : end.status === 'done' ? 'done' : end.status === 'budget-stopped' ? 'budget-stopped'
+ : end.status === 'failed' || end.status === 'interrupted' ? 'failed' : end.status;
+ return {
+ state,
+ reason: end ? (end.reason || null) : null,
+ resolved: end ? (end.resolved || null) : null,
+ attempt: end ? (end.attempt || 1) : 0,
+ path: (end && end.resolved && end.resolved.path) || [],
+ end: end ? { key: end.key, runner: end.runner, name: end.name, status: end.status } : null,
+ step: step(runs, end, opts),
+ attempts: runs,
+ };
+}
+
+module.exports = {
+ RUNNERS, SWITCHABLE, REASON_TEXT,
+ reason, base, chainRoot, sameChain,
+ annotate, settle, nextRunner, continuationNote,
+ facts, detect, nextStep, outcome,
+};
new file mode 100644
new file mode 100644
@@ -0,0 +1,148 @@
@@ -0,0 +1,148 @@
+'use strict';
+
+/**
+ * Reasoning effort and blocked models: the two places where a runner is handed something it will refuse.
+ *
+ * Effort: every harness accepts a different set of levels. The 2026-10-29 incident: a job retried on another runner
+ * copied Codex's "medium" to Command Code, which died before its first step ("Unknown effort \"medium\". Supported: off,
+ * low, high, max."). `fitEffort` rounds a wanted level to the nearest one the target harness accepts (a tie goes up), so
+ * no caller can hand a runner a level it refuses. The levels are uneven ("off" and "ultra" exist on one harness only),
+ * so the ordering must know all of them or a cross-runner value passes through untouched.
+ *
+ * Blocked models: a provider can refuse a model on one account ("not supported with a ChatGPT account"). The list is
+ * per harness; a blocked model is replaced by another one the harness still lists, and a launch with no unblocked model
+ * left is an error, never a silent run with a refused model.
+ *
+ * The keys are the portal's short runner ids, so the portal's own ladders and catalogs keep working. Codes calls the
+ * same harnesses by their full names; `harnessOf` maps those onto the short ids.
+ */
+
+/** The reasoning levels each harness accepts (empty: the harness has no setting). */
+const EFFORTS = {
+ cx: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'],
+ cc: ['off', 'low', 'high', 'max'],
+ mm: ['low', 'medium', 'high', 'xhigh', 'max'],
+ cm: ['low', 'medium', 'high', 'xhigh', 'max'],
+ pg: ['low', 'medium', 'high', 'max'],
+ cd: ['low', 'high', 'max'],
+ claude: ['low', 'medium', 'high', 'xhigh', 'max'],
+ dsh: ['none', 'low', 'high', 'max'],
+ oc: [],
+};
+
+/** Codes' harness ids and the portal's short runner ids for the same harness. */
+const ALIASES = {
+ 'claude-code': 'claude',
+ codex: 'cx',
+ 'command-code': 'cc',
+ opencode: 'oc',
+ deepseek: 'dsh',
+ minimax: 'mm',
+ grove: 'pg',
+};
+
+/** Every level, from the weakest to the strongest. */
+const ORDER = ['off', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'];
+
+/** The short runner id of a harness, whoever named it. */
+function harnessOf(id) {
+ const key = String(id || '').toLowerCase();
+ return ALIASES[key] || key;
+}
+
+/**
+ * The nearest level the harness accepts for the wanted one (a tie goes up).
+ *
+ * @param {string} runner the harness
+ * @param {string|null} effort the wanted level
+ * @returns {string|null} a level the harness accepts, or the wanted one when the harness has no effort setting
+ */
+function fitEffort(runner, effort) {
+ const list = EFFORTS[harnessOf(runner)] || [];
+ if (!effort || !list.length || list.includes(effort)) return effort;
+ const at = ORDER.indexOf(effort);
+ if (at < 0) return effort;
+ for (let d = 0; d < ORDER.length; d++) for (const i of [at + d, at - d]) if (list.includes(ORDER[i])) return ORDER[i];
+ return effort;
+}
+
+/**
+ * One reasoning level up, for a retry of a real failure: the same attempt again rarely helps.
+ *
+ * @param {string|null} effort the level this attempt used
+ * @param {string[]|null} list the levels the runner accepts (null: any)
+ * @returns {string|null} the next level
+ */
+function stepUp(effort, list) {
+ const order = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
+ const i = order.indexOf(effort || 'medium');
+ for (let j = i + 1; j < order.length; j++) if (!list || list.includes(order[j])) return order[j];
+ return effort || null;
+}
+
+/** The blocked models of one harness. */
+function blockedList(blockedModels, runner) {
+ const list = (blockedModels || {})[harnessOf(runner)];
+ return Array.isArray(list) ? list : [];
+}
+
+/** Is this model refused by the provider on this harness's account? */
+function isBlocked(blockedModels, runner, model) {
+ if (!model) return false;
+ return blockedList(blockedModels, runner).includes(model);
+}
+
+/** The catalog of one harness, by either of its names. */
+function catalogFor(models, runner) {
+ const all = models || {};
+ return all[runner] || all[harnessOf(runner)] || [];
+}
+
+/**
+ * The model a launch may use: the wanted one, unless the provider refused it, then the first the harness still lists.
+ *
+ * @param {object} what
+ * @param {string} what.runner the harness
+ * @param {string|null} what.model the wanted model
+ * @param {object} what.models the catalogs, per harness
+ * @param {object} what.blockedModels the blocked lists, per harness
+ * @returns {string|null} the model to launch with
+ * @throws {Error} when every model the harness lists is blocked
+ */
+function pickModel({ runner, model, models, blockedModels } = {}) {
+ const blocked = blockedList(blockedModels, runner);
+ if (!model || !blocked.includes(model)) return model;
+ const replacement = catalogFor(models, runner).find((m) => !blocked.includes(m));
+ if (!replacement) throw new Error(`No unblocked model available for ${runner}`);
+ return replacement;
+}
+
+/** The provider messages that mean "this model is not available to you". */
+const REFUSAL_RE = /not supported when using|model_not_found|not available on your plan|model_not_in_plan/i;
+
+/**
+ * The blocked list after a refusal: the model is added when the provider said it does not have it, and left alone
+ * otherwise (a crash is not a missing model).
+ *
+ * @param {string[]} list the current blocked list
+ * @param {string} model the model that was refused
+ * @param {string} error the provider's message
+ * @returns {string[]} the new list
+ */
+function blockModel(list, model, error) {
+ if (!model || !REFUSAL_RE.test(String(error || ''))) return Array.isArray(list) ? list : [];
+ const current = Array.isArray(list) ? list : [];
+ if (current.includes(model)) return current;
+ return [...current, model];
+}
+
+/** The blocked list without one model. */
+function unblockModel(list, model) {
+ return (Array.isArray(list) ? list : []).filter((m) => m !== model);
+}
+
+module.exports = {
+ EFFORTS, ALIASES, ORDER, REFUSAL_RE,
+ harnessOf, fitEffort, stepUp,
+ blockedList, isBlocked, catalogFor, pickModel, blockModel, unblockModel,
+};
new file mode 100644
new file mode 100644
@@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
+'use strict';
+
+/**
+ * The agent engine: the pieces of the private agent portal that Codes, the CLI and the hosted runner all share.
+ *
+ * engine.router what kind of task is this, and which runner may take it
+ * engine.chain what each attempt did, why it stopped, and what happens next (pure)
+ * engine.budget how far one task may go, and the wrap-up turn that lands the work
+ * engine.transcript one event shape for every harness's log
+ * engine.effort the reasoning level a harness accepts, and the models a provider refuses
+ * engine.scope one systemd scope per job, so nothing is left behind
+ * engine.runerror what a failed run really said
+ *
+ * The modules do not depend on each other in a cycle, and none of them reads the portal's log folder.
+ */
+
+const router = require('./router');
+const chain = require('./chain');
+const budget = require('./budget');
+const transcript = require('./transcript');
+const effort = require('./effort');
+const scope = require('./scope');
+const runerror = require('./runerror');
+
+module.exports = {
+ router,
+ chain,
+ budget,
+ transcript,
+ effort,
+ scope,
+ runerror,
+ // The names a caller usually wants, in one place.
+ classify: router.classify,
+ pick: router.pick,
+ outcome: chain.outcome,
+ nextStep: chain.nextStep,
+ evaluate: budget.evaluate,
+ limitsFor: budget.limitsFor,
+ parse: transcript.parse,
+ parseText: transcript.parseText,
+ fitEffort: effort.fitEffort,
+ pickModel: effort.pickModel,
+};
new file mode 100644
new file mode 100644
@@ -0,0 +1,171 @@
@@ -0,0 +1,171 @@
+'use strict';
+
+/**
+ * The tier router: what kind of job is this, and which runner may take it.
+ *
+ * A brief is classified into one of five tiers — basic, research, standard, hard, design — and each tier has its own
+ * ladder, cheapest suitable first: free models for basic chores, flat-price plans for everyday code, the strongest
+ * models only for hard problems and design. A brief can force its tier with a line "tier: basic|research|standard|hard|
+ * design".
+ *
+ * Only runners that are on, healthy and have a free slot are chosen, and a runner whose recent jobs mostly failed for
+ * real is skipped until it does better (the record comes from the caller's run list, never from a file here). Pay-per-
+ * token DeepSeek comes after the plans that are already paid for.
+ *
+ * The ladder below is the portal's, so the portal's jobs keep their routing. Codes supplies its own ladder (its
+ * harness ids differ) through `pick(prompt, { ladder })`.
+ */
+
+const fs = require('fs');
+const path = require('path');
+const { fitEffort, stepUp, EFFORTS } = require('./effort');
+
+const LADDER = {
+ // Chores: renames, docs, formatting, counting, simple checks — free and plan Flash models first.
+ basic: [['oc', 'opencode-go/beam-free', null], ['cc', 'reflection/beam:free', 'low'], ['pg', 'deepseek-v4.1-flash-1017', 'low'], ['mm', null, 'low'], ['cx', 'gpt-6-luna', 'low'], ['dsh', 'deepseek-flash', 'none']],
+ // Reading, comparing, web research, audits: models with search, middle reasoning.
+ research: [['cx', 'gpt-6-sol', 'medium'], ['mm', null, 'medium'], ['dsh', 'deepseek-flash', 'high'], ['cc', 'deepseek/deepseek-v4.1-flash', 'high'], ['oc', 'opencode-go/kimi-k3', null]],
+ // Everyday features and fixes: flat-price plans first.
+ standard: [['mm', null, 'high'], ['cx', 'gpt-6-sol', 'medium'], ['cc', 'deepseek/deepseek-v4.1-flash', 'high'], ['oc', 'opencode-go/kimi-k2.7-code', null], ['dsh', 'deepseek-flash', 'high']],
+ // Root causes, architecture, security, migrations: the strongest models.
+ // DeepSeek V4 Pro only as the very last resort (owner, 10-29: it costs a lot more and rarely earns it).
+ hard: [['cx', 'gpt-6-astra', 'high'], ['mm', null, 'max'], ['cc', 'deepseek/deepseek-v4.1-flash', 'high'], ['oc', 'opencode-go/kimi-k3.1-pro', null], ['dsh', 'deepseek-flash', 'max'], ['dsh', 'deepseek-v4-pro', 'max']],
+ // Layout, styling, UI: strong models with taste (Opus reviews later through OPUS-REVIEW-QUEUE.md).
+ // Astra only in the hard tier (owner, 10-29: "does it really need Astra for this?").
+ design: [['cx', 'gpt-6-sol', 'high'], ['mm', null, 'high'], ['cc', 'deepseek/deepseek-v4.1-flash', 'high'], ['dsh', 'deepseek-flash', 'high']],
+};
+
+/** The tiers a brief can fall into. */
+const TIERS = ['basic', 'research', 'standard', 'hard', 'design'];
+
+// The brief a launch prompt points at: "read /…/agents/jobs/x.brief.md and follow it" is a pointer, not a task, so
+// the job is judged by the brief it names (2026-10-29: a pin job was routed as "a short question" to a free model
+// because its prompt was one sentence long). The job's own brief counts too (jobs/<name>.brief.md, jobs/<name>.task.md).
+const BRIEF_RE = /(?:^|[\s'"`(])([^\s'"`)]*\/jobs\/[\w.-]+\.(?:md|txt))/g;
+
+/**
+ * The brief a prompt names, read from the jobs folder.
+ *
+ * @param {string} prompt the launch prompt
+ * @param {string} JOBS the jobs folder
+ * @param {string} [name] the job's own name
+ * @returns {{text: string, file: string}|null} the brief, or null when there is none
+ */
+function briefFor(prompt, JOBS, name) {
+ const text = String(prompt || '');
+ const cands = [];
+ for (const m of text.matchAll(BRIEF_RE)) {
+ const raw = m[1];
+ cands.push(path.isAbsolute(raw) ? raw : path.join(JOBS, path.basename(raw)));
+ }
+ if (name) cands.push(path.join(JOBS, `${name}.brief.md`), path.join(JOBS, `${name}.task.md`));
+ for (const p of cands) {
+ if (!p) continue;
+ let t = null;
+ try { t = fs.readFileSync(p, 'utf8'); } catch { continue; }
+ if (!t || !t.trim() || t.trim() === text.trim()) continue; // the prompt itself is no brief
+ return { text: t, file: p };
+ }
+ return null;
+}
+
+/** The tier a line forces, if it forces one. */
+function forcedTier(text) {
+ const m = String(text || '').match(/^\s*tier:\s*(basic|research|standard|hard|design)\b/im);
+ return m ? m[1].toLowerCase() : null;
+}
+
+/**
+ * The tier of a brief, from the brief itself.
+ *
+ * @param {string} text the brief (or the prompt)
+ * @param {object} [opts]
+ * @param {string} [opts.brief] the file the brief came from
+ * @param {boolean} [opts.whole] also look for the tier line past the first 4000 characters (a brief may be long)
+ * @returns {{tier: string, why: string, brief?: string}} the tier and the words that say so
+ */
+function classifyText(text, opts = {}) {
+ const p = String(text || ''), head = p.slice(0, 4000);
+ const said = opts.brief ? { brief: opts.brief } : {};
+ const forced = forcedTier(head) || (opts.whole ? forcedTier(p) : null);
+ if (forced) return { tier: forced, why: 'the brief says so', ...said };
+ if (/\b(race condition|root cause|architecture|security (review|fix|hole)|migration|data loss|deadlock|concurrency bug|memory leak|performance regression|hard bug)\b/i.test(head)) return { tier: 'hard', why: 'a hard problem', ...said };
+ if (/\b(design pass|redesign|layout|styling|visual|polish the|ui\b|ux\b|css|look and feel|screenshots? at 390)\b/i.test(head)) return { tier: 'design', why: 'design work', ...said };
+ if (/\b(research|investigate|survey|compare|audit|competitor|do not edit any repository|report only|read-only)\b/i.test(head)) return { tier: 'research', why: 'research or audit', ...said };
+ // A chore only when it reads like one and asks for nothing to be built (a short brief can still be a real feature).
+ const builds = /\b(add|build|implement|create|endpoint|api|feature|refactor|migrat|tests?|component|page|service|integrat)\b/i.test(head);
+ if (p.length < 1500 && !builds && /\b(rename|typo|reformat|format|bump|changelog|readme|docs? (update|fix)|add (a )?comment|count|list (all|the)|summari[sz]e|translate|check (that|whether|if)|lint|spell)\b/i.test(head)) return { tier: 'basic', why: 'a small chore', ...said };
+ if (p.length < 200 && !builds) return { tier: 'basic', why: 'a short question', ...said };
+ return { tier: 'standard', why: 'everyday code', ...said };
+}
+
+/**
+ * The tier of a job: from the brief its prompt names, or from the prompt itself.
+ *
+ * @param {string} prompt the launch prompt
+ * @param {object} [opts]
+ * @param {string} [opts.JOBS] the jobs folder, where briefs live
+ * @param {string} [opts.name] the job's own name
+ * @param {string} [opts.briefText] the brief's text, when the caller already has it
+ * @returns {{tier: string, why: string, brief?: string}} the tier and the words that say so
+ */
+function classify(prompt, opts = {}) {
+ const brief = opts.briefText ? { text: opts.briefText, file: opts.briefFile || null }
+ : (opts.JOBS ? briefFor(prompt, opts.JOBS, opts.name) : null);
+ if (!brief) return classifyText(String(prompt || ''));
+ // The tier line of the brief wins wherever it stands in it.
+ return classifyText(brief.text, { brief: brief.file || undefined, whole: true });
+}
+
+/**
+ * The record of what each runner really achieved: a runner whose recent jobs (7 days, at least 4) mostly failed for
+ * real (not limits or sign-in, which the runner switch handles) is skipped until it does better.
+ *
+ * @param {object[]} runs the runs, newest first
+ * @returns {object} per runner: { done, failed }
+ */
+function track(runs) {
+ const since = Date.now() - 7 * 86400e3, rec = {};
+ for (const r of runs || []) {
+ if (r.session || r.runner === 'ship' || r.updated < since || !['done', 'failed', 'interrupted'].includes(r.status)) continue;
+ if (r.status !== 'done' && ['limit', 'auth', 'capacity', 'refused'].includes(r.reason)) continue;
+ const t = rec[r.runner] || (rec[r.runner] = { done: 0, failed: 0 });
+ // Resolution belongs to the job chain, not this runner's attempt. A rescue
+ // by another run (or an owner note) must not turn its failure into success.
+ if (r.status === 'done') t.done++; else t.failed++;
+ }
+ return rec;
+}
+
+/**
+ * The runner, model and reasoning for a job, or null when nothing on the ladder can take it now.
+ *
+ * @param {string} prompt the brief or the launch prompt
+ * @param {object} ctx
+ * @param {object} ctx.cfg the config (subagents: per runner { enabled, max, model })
+ * @param {object} ctx.health the health of each runner ({ state })
+ * @param {object} ctx.counts how many jobs each runner runs now
+ * @param {object} ctx.models the catalogs of each runner
+ * @param {object[]} [ctx.runs] the recent runs, for the reliability gate
+ * @param {string} [ctx.JOBS] the jobs folder
+ * @param {string} [ctx.name] the job's own name
+ * @param {object} [ctx.ladder] the ladder to use (the portal's by default)
+ * @returns {{runner: string, model: string|null, effort: string|null, tier: string, brief: string|null, why: string}|null}
+ */
+function pick(prompt, { cfg, health, counts, models, runs, JOBS, name, ladder } = {}) {
+ const c = classify(prompt, { JOBS, name });
+ const rec = track(runs);
+ const reliable = (r) => { const t = rec[r]; return !t || t.done + t.failed < 4 || t.done / (t.done + t.failed) >= 0.4; };
+ const usable = (r) => cfg.subagents[r] && cfg.subagents[r].enabled && (counts[r] || 0) < cfg.subagents[r].max && health[r] && health[r].state === 'ok' && reliable(r);
+ for (const [runner, model, wanted] of (ladder || LADDER)[c.tier]) {
+ if (!usable(runner)) continue;
+ // A model the runner no longer lists is skipped for that runner's default (lists refresh from each provider).
+ const m = model && models[runner] && !models[runner].includes(model) ? null : model;
+ const effort = fitEffort(runner, wanted);
+ const judged = c.brief ? `${c.why} (the brief it names)` : c.why;
+ return { runner, model: m, effort, tier: c.tier, brief: c.brief || null, why: `${judged} → ${runner}${m ? ' · ' + m : ''}${effort ? ' · ' + effort : ''}` };
+ }
+ return null;
+}
+
+module.exports = { LADDER, TIERS, classify, classifyText, pick, track, briefFor, forcedTier, fitEffort, stepUp, EFFORTS };
new file mode 100644
new file mode 100644
@@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
+'use strict';
+
+/**
+ * What a failed run really said.
+ *
+ * A harness that ends without an answer often leaves the wrapper's own no-result message behind
+ * ("Command Code ended without a result (credits, rate limit or a crash: check its exit code)"). That message lists
+ * possible causes, not an observed failure, so it is never reported as one: only a real diagnostic in the feed or on
+ * stderr is. The result is the work's own error, never a fragment of the launch command (the brief is echoed after a
+ * killed child).
+ */
+
+/** The wrapper's no-result message: possible causes, not an observed quota failure. */
+const NO_RESULT = /Command Code ended without a result\s*\(credits, rate limit or a crash: check its exit code\)/i;
+
+const MAX = 200;
+
+/** A message of any shape as one line of text. */
+function message(value) {
+ if (typeof value === 'string') return value;
+ if (Array.isArray(value)) return value.map(message).filter(Boolean).join('; ');
+ return value && typeof value === 'object' ? message(value.message || value.error) : '';
+}
+
+/**
+ * The error of a failed run, from its feed, its stderr and a summary.
+ *
+ * @param {string} feed the run's jsonl feed
+ * @param {string} stderr what the launcher wrote
+ * @param {string} summary a fallback message
+ * @returns {string} the error, at most 200 characters ('' when there was none)
+ */
+function failureError(feed, stderr, summary = '') {
+ let result = '';
+ // Parse complete JSON records: escaped quotes and whitespace are legal, and tool output is not a run failure.
+ for (const line of String(feed || '').split('\n')) {
+ let e; try { e = JSON.parse(line); } catch { continue; }
+ if (e.type === 'result' && e.is_error) result = message(e.result) || message(e.errors);
+ }
+ if (result && !NO_RESULT.test(result)) return result.trim().slice(0, MAX);
+ const lines = String(stderr || '').split('\n');
+ let diagnostic = '';
+ for (const line of lines) {
+ // Bash prints the entire launch command (including a multiline brief) after a killed child.
+ // Keep only its signal diagnosis, never a fragment of the echoed command or brief.
+ const signal = line.match(/:\s*line \d+:\s*\d+\s+(Killed|Terminated|Segmentation fault|Aborted)\b/);
+ if (signal) return signal[1];
+ if (/^(?:API Error:|Error:|Refused:|mcode exec (?:failed|cancelled):|\[claude-code:[\w-]+\]|(?:Killed|Terminated)\.?$)/.test(line.trim())) diagnostic = line.trim();
+ }
+ return (diagnostic || result || message(summary)).trim().slice(0, MAX);
+}
+
+module.exports = { failureError, message, NO_RESULT };
new file mode 100644
new file mode 100644
@@ -0,0 +1,117 @@
@@ -0,0 +1,117 @@
+'use strict';
+// Every job and chat turn runs in its own systemd scope inside ovjobs.slice (2026-10-30).
+//
+// Why: an agent that starts a server in the background (`node server/index.js &`, setsid, nohup) leaves it behind when
+// its shell exits. The server is reparented to the user's systemd, so it is no longer in the job's process group or
+// tree and procgroup.stopTree never sees it. Fifteen such servers and headless browsers (8-16 h old, ~7 GB) ran the
+// workstation out of memory on 2026-10-30 01:38 UTC: the kernel killed the IDE, the portal and every running job.
+//
+// A scope is a cgroup: everything the job starts stays in it, however it detaches. When the job's own process is gone,
+// reap() stops the scope, which ends whatever is left. ovjobs.slice caps all jobs together (MemoryHigh 15G, MemoryMax
+// 18G) and each scope caps one job (6G/8G, OOMPolicy=continue: an out-of-memory kill takes one process, like a test
+// runner, not the whole job), so no job can take the machine down again.
+//
+// `systemd-run --scope` execs the command in place: the pid, process group, cwd, env and stdio are the same as a
+// plain spawn, so the rest of the runner (pid files, stopTree) is unchanged. Without systemd-run (or a user manager)
+// wrap() returns the command as it was, and this module does nothing anywhere but Linux.
+
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { execFile, execFileSync } = require('child_process');
+
+const SLICE = 'ovjobs.slice';
+const PER_JOB = ['-p', 'MemoryHigh=6G', '-p', 'MemoryMax=8G', '-p', 'OOMPolicy=continue', '-p', 'TimeoutStopSec=15'];
+
+// Where the map of scopes lives: which process leads which scope, so reap() knows when a job itself is over.
+const MAP = process.env.OV_JOB_SCOPE_MAP || path.join(os.homedir(), '.openvibe-codes', 'job-scopes.json');
+
+let usable = null;
+
+/** Are scopes available here? (Linux with a systemd user manager: false everywhere else.) */
+function available() {
+ if (usable !== null) return usable;
+ if (process.platform !== 'linux') return (usable = false);
+ if (process.env.OV_NO_JOB_SCOPES) return (usable = false);
+ try {
+ execFileSync('systemd-run', ['--user', '--scope', '--quiet', '--collect', `--slice=${SLICE}`, '--', 'true'], { timeout: 8000, stdio: 'ignore' });
+ usable = true;
+ } catch { usable = false; }
+ return usable;
+}
+
+const unitName = (label) => `ovjob-${String(label || 'job').replace(/[^A-Za-z0-9_-]+/g, '_').slice(0, 90)}-${Date.now().toString(36)}.scope`;
+
+/**
+ * The command to spawn instead of (cmd, args), and the scope it will run in (null when scopes are not available).
+ *
+ * @param {string} cmd the command
+ * @param {string[]} args its arguments
+ * @param {string} label a name for the scope
+ * @returns {{cmd: string, args: string[], unit: string|null}} what to spawn
+ */
+function wrap(cmd, args, label) {
+ if (!available()) return { cmd, args, unit: null };
+ const unit = unitName(label);
+ return {
+ cmd: 'systemd-run',
+ args: ['--user', '--scope', '--quiet', '--collect', `--unit=${unit.replace(/\.scope$/, '')}`, `--slice=${SLICE}`, `--description=ovjob ${label}`, ...PER_JOB, '--', cmd, ...args],
+ unit,
+ };
+}
+
+function load() { try { return JSON.parse(fs.readFileSync(MAP, 'utf8')); } catch { return {}; } }
+function save(map) {
+ try {
+ fs.mkdirSync(path.dirname(MAP), { recursive: true });
+ fs.writeFileSync(`${MAP}.tmp`, JSON.stringify(map));
+ fs.renameSync(`${MAP}.tmp`, MAP);
+ } catch { /* best effort */ }
+}
+
+/** Record which process leads a scope, so reap() knows when the job itself is over. */
+function remember(unit, pid, label) {
+ if (!unit || !(pid > 1)) return;
+ const map = load();
+ map[unit] = { pid, label: label || '', at: Date.now() };
+ save(map);
+}
+
+const run = (args) => new Promise((resolve) => execFile('systemctl', ['--user', ...args], { timeout: 20000 }, (e, out) => resolve(e ? null : String(out || ''))));
+
+// The scope's leader is still the job: alive and still in that cgroup (a reused pid is somebody else).
+function leads(pid, unit) {
+ try { return fs.readFileSync(`/proc/${pid}/cgroup`, 'utf8').includes(`/${unit}`); } catch { return false; }
+}
+
+/**
+ * Stop every scope whose job has ended, ending what it left behind.
+ *
+ * @param {object} [opts]
+ * @param {Function} [opts.log] where the notice goes
+ * @returns {Promise<string[]>} the scopes stopped
+ */
+async function reap({ log = () => {} } = {}) {
+ if (!available()) return [];
+ const out = await run(['list-units', '--type=scope', '--plain', '--no-legend', '--all', 'ovjob-*']);
+ if (out === null) return [];
+ const live = new Set(out.split('\n').map((l) => l.trim().split(/\s+/)[0]).filter((u) => /^ovjob-.*\.scope$/.test(u)));
+ const map = load();
+ const stopped = [];
+ for (const [unit, rec] of Object.entries(map)) {
+ if (!live.has(unit)) { delete map[unit]; continue; } // gone already (nothing was left behind)
+ if (leads(rec.pid, unit)) continue; // the job is still running
+ if (await run(['stop', unit]) !== null) { stopped.push(unit); delete map[unit]; log(`jobscope: ${rec.label || unit} ended; stopped its scope (left-behind processes)`); }
+ }
+ save(map);
+ return stopped;
+}
+
+/** Stop one job's scope now (the owner pressed Stop, or the budget ended it). */
+async function stopFor(pid) {
+ const map = load();
+ for (const [unit, rec] of Object.entries(map)) if (rec.pid === Number(pid)) { await run(['stop', unit]); delete map[unit]; save(map); return unit; }
+ return null;
+}
+
+module.exports = { available, wrap, remember, reap, stopFor, SLICE, MAP };
new file mode 100644
new file mode 100644
@@ -0,0 +1,434 @@
@@ -0,0 +1,434 @@
+'use strict';
+
+/**
+ * One conversation shape for every harness's log, so a task reads the same whoever worked on it.
+ *
+ * The harnesses speak different languages: Claude Code stream-json (Claude, Claude on DeepSeek), the runners'
+ * converted feeds (Codex, Command Code, OpenCode Go), DeepSeek's `dsh --json` and MiniMax Code's
+ * `mcode exec --output-format stream-json`. This module turns all of them into one event shape:
+ *
+ * { t, text, name, input, output, at, … }
+ *
+ * `t` is the kind of event ('user', 'text', 'thinking', 'tool', 'error', 'final', 'meta'), `at` is when it happened
+ * (ISO 8601), and a tool event carries its `name`, its `input` and, once it arrives, its `output`. The whole shape is
+ * described in transcript.schema.json.
+ *
+ * Parsing is incremental per file: only the bytes added since the last read are parsed, so polling a running task
+ * every second or two stays cheap. `parseText` does the same for a stream that is already in memory.
+ */
+
+const fs = require('fs');
+
+const OUT_CAP = 8000;
+const CACHE_BYTES = 32 * 1024 * 1024;
+const cache = new Map();
+let cacheBytes = 0;
+
+/** Forget a file's parsed state (its log was truncated or removed). */
+function forget(file) {
+ const entry = cache.get(file);
+ if (entry) cacheBytes -= entry.bytes;
+ cache.delete(file);
+}
+
+/** Remember a file's parsed state, within the cache's size. */
+function remember(file, state) {
+ forget(file);
+ const bytes = state.events.reduce((total, event) => total + JSON.stringify(event).length * 2 + 512, 1024)
+ + state.byId.size * 128;
+ if (bytes > CACHE_BYTES) return;
+ while (cache.size >= 60 || cacheBytes + bytes > CACHE_BYTES) forget(cache.keys().next().value);
+ cache.set(file, { state, bytes });
+ cacheBytes += bytes;
+}
+
+/** A value as text, clipped, with the ANSI colours stripped. */
+function clip(v, n = OUT_CAP) {
+ let s = typeof v === 'string' ? v : v == null ? '' : JSON.stringify(v, null, 2);
+ s = s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
+ return s.length > n ? { text: s.slice(0, n), more: s.length - n } : { text: s, more: 0 };
+}
+
+/** The kind of a tool, from its name. */
+function kindOf(name) {
+ const n = String(name || '').toLowerCase();
+ if (/bash|shell|exec|command|terminal|run_?cmd/.test(n)) return 'shell';
+ if (/^(read|view|cat|read_?file|open)/.test(n)) return 'read';
+ if (/edit|write|patch|create_?file|replace|multiedit|str_replace/.test(n)) return 'edit';
+ if (/grep|glob|search_?files|find|list|ls|rg/.test(n) && !/web/.test(n)) return 'search';
+ if (/web|fetch|browse|url|http/.test(n)) return 'web';
+ if (/todo|task|agent|plan/.test(n)) return 'plan';
+ return 'tool';
+}
+
+/** One line about what a tool was asked to do. */
+function summary(name, input) {
+ if (input == null) return '';
+ if (typeof input !== 'object') return String(input).replace(/\s+/g, ' ').slice(0, 220);
+ let v = input.command || input.cmd || input.file_path || input.filePath || input.path || input.pattern || input.query || input.url
+ || input.description || input.prompt || Object.values(input).find((x) => typeof x === 'string');
+ if (Array.isArray(v)) v = v.join(' ');
+ v = String(v || '');
+ // Codex wraps every command as /bin/bash -lc "…".
+ const m = v.match(/^\/bin\/(?:ba)?sh -lc (['"])([\s\S]*)\1$/);
+ if (m) v = m[2];
+ // "cd <job folder> && …" says nothing new: show the command itself.
+ v = v.replace(/^\s*cd\s+("[^"]+"|'[^']+'|\S+)\s*(&&|;)\s*/, '');
+ return v.replace(/\s+/g, ' ').slice(0, 220);
+}
+
+/** The text of a content block, of any shape. */
+function textOf(c) {
+ if (c == null) return '';
+ if (typeof c === 'string') return c;
+ if (Array.isArray(c)) return c.map((b) => (typeof b === 'string' ? b : b && (b.text || b.content || (b.type === 'image' ? '[image]' : '')) || '')).join('\n');
+ if (typeof c === 'object') return c.text || c.content || c.output || JSON.stringify(c, null, 2);
+ return String(c);
+}
+
+/** A tool input as data, when it is a JSON string. */
+function inputOf(raw) {
+ if (typeof raw === 'string') { try { return JSON.parse(raw); } catch { return raw; } }
+ return raw;
+}
+
+/** An empty parse state. */
+function freshState() {
+ return { offset: 0, line: 0, events: [], byId: new Map(), session: null, model: null, cwd: null, usage: { input: 0, output: 0, cached: 0 }, cost: null };
+}
+
+/** When an event happened, as ISO 8601. */
+function atOf(ts) {
+ if (ts == null || !Number.isFinite(Number(ts))) return null;
+ try { return new Date(Number(ts)).toISOString(); } catch { return null; }
+}
+
+/** Add an event to the state, merging the ones that belong together. */
+function push(st, ev) {
+ if (ev.at === undefined) ev.at = atOf(ev.ts);
+ const last = st.events[st.events.length - 1];
+ // The same error reported twice in a row (a system error, then the result) shows once.
+ if (ev.t === 'error' && st.events.slice(-3).some((e) => e.t === 'error' && e.text.trim() === String(ev.text).trim())) return st.events[st.events.length - 1];
+ // Consecutive thinking (dsh emits one per step) and streamed message deltas merge into one block.
+ if (last && ev.t === last.t && !ev.mid && !last.mid && (ev.t === 'thinking' || (ev.t === 'text' && ev.delta && last.delta))) {
+ last.text = (last.text + (ev.t === 'thinking' && !ev.delta ? '\n\n' : '') + ev.text).slice(-20000);
+ return last;
+ }
+ ev.i = st.events.length;
+ st.events.push(ev);
+ return ev;
+}
+
+/** Add a tool call. */
+function addTool(st, id, name, input, line, ts) {
+ const inp = inputOf(input);
+ const ev = push(st, { t: 'tool', id: id || `t${st.events.length}`, name: name || 'tool', kind: kindOf(name), summary: summary(name, inp), input: clip(inp, 6000), pending: true, line, ts });
+ st.byId.set(ev.id, ev.i);
+ return ev;
+}
+
+/** Add the result of a tool call. */
+function setResult(st, id, output, isError) {
+ const i = st.byId.get(id);
+ if (i == null) return false;
+ const ev = st.events[i];
+ const out = clip(textOf(output));
+ ev.output = out;
+ ev.pending = false;
+ ev.isError = !!isError || /\[exit code: [1-9]\d*\]|^Exit code [1-9]|\bexit_code["']?:\s*[1-9]/.test(out.text.slice(-200));
+ const m = out.text.match(/\[exit code: (\d+)\]|Exit code (\d+)/);
+ if (m) ev.exit = Number(m[1] || m[2]);
+ ev.v = (ev.v || 0) + 1;
+ return true;
+}
+
+// A streamed piece of a message (Claude Code with --include-partial-messages): it grows in one place and is
+// replaced by the whole message when that arrives, so the same words are never shown twice.
+function pushDelta(st, ev) {
+ for (let i = st.events.length - 1; i >= 0; i--) {
+ const e = st.events[i];
+ if (e.t !== ev.t) continue;
+ if (e.partial) { e.text = (e.text + ev.text).slice(-20000); e.v = (e.v || 0) + 1; return e; }
+ break;
+ }
+ // mid keeps the delta apart from the block before it (push merges two thinking blocks otherwise).
+ const p = { ...ev, partial: true, mid: true };
+ ev.i = st.events.length;
+ st.events.push(p);
+ return p;
+}
+
+const lastPartial = (st, t) => {
+ for (let i = st.events.length - 1; i >= 0; i--) if (st.events[i].t === t && st.events[i].partial) return st.events[i];
+ return null;
+};
+
+function finalEv(st, text, extra, line, ts) {
+ let t = String(text || '').trim();
+ if (t && st.events.slice(-4).some((e) => e.t === 'error' && e.text.trim() === t)) t = '';
+ const last = [...st.events].reverse().find((e) => e.t === 'text');
+ // Claude's result repeats its last message: mark that message as the answer instead of printing it twice.
+ if (last && t && last.text.trim() === t) { last.answer = true; last.v = (last.v || 0) + 1; return push(st, { t: 'final', text: '', ...extra, line, ts }); }
+ return push(st, { t: 'final', text: t, ...extra, line, ts });
+}
+
+// MiniMax repeats a turn's usage in exec.completed, including partial usage when
+// turn.failed has none. Account for each turn once, adding only newly reported tokens.
+function minimaxUsage(st, e, payload = e) {
+ const u = payload.usage;
+ if (!u) return;
+ const key = `${e.runId || payload.runId || ''}:${e.turnId || payload.turnId || ''}`;
+ if (!st.minimaxUsage) st.minimaxUsage = new Map();
+ const previous = st.minimaxUsage.get(key) || { input: 0, output: 0, cached: 0 };
+ const cached = Number(u.cacheReadTokens || u.cachedInputTokens || u.cache_read_input_tokens || 0);
+ const current = { input: Number(u.inputTokens || u.input_tokens || 0) + cached,
+ output: Number(u.outputTokens || u.output_tokens || 0), cached };
+ for (const field of ['input', 'output', 'cached']) {
+ current[field] = Math.max(previous[field], current[field]);
+ st.usage[field] += current[field] - previous[field];
+ }
+ st.minimaxUsage.set(key, current);
+}
+
+/** Read one event of a harness' feed. */
+function handle(st, e, line) {
+ const ts = e._ts || e.timestampMs || (e.timestamp ? Date.parse(e.timestamp) : null) || null;
+ const type = e.type;
+ // MiniMax Code (schemaVersion 1 events).
+ if (e.schemaVersion && typeof type === 'string' && /\./.test(type)) {
+ if (e.sessionId) st.session = e.sessionId;
+ // A tool call arrives as item.started / item.updated / item.completed with its input and output inside toolCall
+ // (status 2 = done); tools run inside Programmatic Tool Calling's run_code carry parentToolCallId.
+ if (/^item\./.test(type) && e.item && e.item.type === 'tool_call' && e.item.toolCall) {
+ const tc = e.item.toolCall, id = tc.id || e.item.id;
+ let i = st.byId.get(id);
+ if (i == null) { const ev = addTool(st, id, tc.name || tc.toolName, tc.input || tc.arguments, line, ts); if (tc.parentToolCallId) ev.nested = true; i = ev.i; }
+ const ev = st.events[i];
+ if (tc.input != null && !ev.summary) { const inp = inputOf(tc.input); ev.input = clip(inp, 6000); ev.summary = summary(ev.name, inp); ev.v = (ev.v || 0) + 1; }
+ if (tc.output != null || type === 'item.completed') setResult(st, id, tc.output != null ? (tc.output.content || tc.output) : '', tc.isError || tc.status === 3 || /fail|error/i.test(String(tc.status)));
+ return;
+ }
+ // A message or thought takes its place when it starts (so it precedes the tools it announces) and its text when complete.
+ if (type === 'item.started' && e.item && (e.item.type === 'agent_message' || e.item.type === 'reasoning')) {
+ const ev = push(st, { t: e.item.type === 'agent_message' ? 'text' : 'thinking', text: '', mid: e.item.id, line, ts });
+ st.byId.set('m:' + e.item.id, ev.i);
+ return;
+ }
+ if (type === 'item.completed' && e.item && st.byId.has('m:' + e.item.id)) {
+ const ev = st.events[st.byId.get('m:' + e.item.id)];
+ ev.text = textOf(e.item.content || e.item.contentDelta) || ev.text; ev.v = (ev.v || 0) + 1;
+ return;
+ }
+ if (type === 'item.completed' && e.item) {
+ const it = e.item;
+ if (it.type === 'agent_message') push(st, { t: 'text', text: textOf(it.content || it.contentDelta), line, ts });
+ else if (it.type === 'reasoning') push(st, { t: 'thinking', text: textOf(it.content || it.contentDelta), line, ts });
+ else if (/tool_result|tool_output/.test(it.type)) setResult(st, it.toolCallId || it.callId || it.id, it.output || it.content || it.result, it.isError || it.status === 'failed');
+ } else if (type === 'turn.completed') {
+ if (e.model && e.model.modelId) st.model = e.model.modelId;
+ minimaxUsage(st, e);
+ } else if (type === 'turn.failed') push(st, { t: 'error', text: textOf(e.error && (e.error.message || e.error)) || `turn ${e.status || 'failed'}`, line, ts });
+ else if (type === 'exec.completed' && e.result) {
+ if (e.result.model && e.result.model.modelId) st.model = e.result.model.modelId;
+ minimaxUsage(st, e, e.result);
+ finalEv(st, e.result.answer || e.result.output || '', { isError: e.result.status && e.result.status !== 'succeeded', ms: e.result.durationMs }, line, ts);
+ }
+ return;
+ }
+ // DeepSeek's official harness (dsh --json).
+ if (type === 'session' && e.sessionId) { st.session = e.sessionId; st.cwd = e.cwd || st.cwd; push(st, { t: 'meta', kind: 'start', session: e.sessionId, cwd: e.cwd, line, ts }); return; }
+ if (type === 'tool_call') { addTool(st, e.callId, e.tool || e.name, e.input || e.arguments, line, ts); return; }
+ if (type === 'tool_result' && e.callId) { setResult(st, e.callId, e.result != null ? e.result : e.output, e.status && e.status !== 'completed'); return; }
+ if (type === 'thinking') { push(st, { t: 'thinking', text: String(e.text || ''), line, ts }); return; }
+ if (type === 'text' && typeof e.text === 'string') { push(st, { t: 'text', text: e.text, line, ts }); return; }
+ if (type === 'final') { finalEv(st, e.text, {}, line, ts); return; }
+ if (type === 'status') { if (e.usage && e.phase === 'step_end') { const c = Number(e.usage.cacheReadTokens || 0); st.usage.input += Number(e.usage.inputTokens || e.usage.promptTokens || 0) + c; st.usage.output += Number(e.usage.outputTokens || e.usage.completionTokens || 0); st.usage.cached += c; } return; }
+ if (type === 'error') { push(st, { t: 'error', text: textOf(e.message || e.text || e.error), line, ts }); return; }
+ // Claude Code stream-json and the runners' feeds (the same shape).
+ if (type === 'system') {
+ if (e.subtype === 'init') {
+ st.session = e.session_id || st.session; st.model = e.model || st.model; st.cwd = e.cwd || st.cwd;
+ push(st, { t: 'meta', kind: 'start', session: e.session_id, model: e.model, backend: e.backend, cwd: e.cwd, permission: e.permissionMode, line, ts });
+ } else if (e.subtype === 'error') push(st, { t: 'error', text: String(e.text || e.message || 'error'), line, ts });
+ else if (e.subtype === 'usage' && e.usage) { st.usage.input += Number(e.usage.input || 0); st.usage.output += Number(e.usage.output || 0); st.usage.cached += Number(e.usage.cached || 0); }
+ else if (e.subtype === 'compact_boundary') push(st, { t: 'meta', kind: 'compact', line, ts });
+ return;
+ }
+ // Claude Code's partial messages: the text and the thinking arrive as deltas while the turn runs.
+ if (type === 'stream_event') {
+ const ev = e.event || {};
+ if (ev.type === 'content_block_delta') {
+ const d = ev.delta || {};
+ if (d.type === 'text_delta' && d.text) pushDelta(st, { t: 'text', text: d.text, line, ts });
+ else if (d.type === 'thinking_delta' && (d.thinking || d.text)) pushDelta(st, { t: 'thinking', text: d.thinking || d.text, line, ts });
+ }
+ return;
+ }
+ if (type === 'assistant' && e.message) {
+ if (e.message.model && e.message.model !== '<synthetic>') st.model = e.message.model; // Claude Code's own error messages say <synthetic>
+ const u = e.message.usage;
+ // Command Code's feed says includes_cache: input_tokens already holds the cached tokens, so adding them again
+ // doubled its prompt volume and halved its shown cache rate (session-analysis 2026-10-29 §1.1).
+ if (u) { const inc = u.includes_cache === true; st.usage.input += Number(u.input_tokens || 0) + (inc ? 0 : Number(u.cache_read_input_tokens || 0) + Number(u.cache_creation_input_tokens || 0)); st.usage.output += Number(u.output_tokens || 0); st.usage.cached += Number(u.cache_read_input_tokens || 0); }
+ const content = Array.isArray(e.message.content) ? e.message.content : [{ type: 'text', text: textOf(e.message.content) }];
+ for (const b of content) {
+ if (!b) continue;
+ if (b.type === 'text' && b.text && b.text.trim()) {
+ // The whole message replaces what was streamed, so the words appear once.
+ const p = lastPartial(st, 'text');
+ if (p) { p.text = b.text; p.partial = false; p.v = (p.v || 0) + 1; }
+ else push(st, { t: 'text', text: b.text, line, ts });
+ }
+ else if ((b.type === 'thinking' || b.type === 'reasoning') && (b.thinking || b.text)) {
+ const p = lastPartial(st, 'thinking');
+ if (p) { p.text = b.thinking || b.text; p.partial = false; p.v = (p.v || 0) + 1; }
+ else push(st, { t: 'thinking', text: b.thinking || b.text, line, ts });
+ }
+ else if (b.type === 'tool_use') addTool(st, b.id, b.name, b.input, line, ts);
+ }
+ return;
+ }
+ if (type === 'user' && e.message) {
+ const c = e.message.content;
+ if (typeof c === 'string') { if (c.trim()) push(st, { t: 'user', text: c, line, ts }); return; }
+ for (const b of c || []) {
+ if (b && b.type === 'tool_result') setResult(st, b.tool_use_id, b.content, b.is_error);
+ else if (b && b.type === 'text' && b.text && !/^<|^\[Request interrupted/.test(b.text.trim())) push(st, { t: 'user', text: b.text, line, ts });
+ }
+ return;
+ }
+ if (type === 'result') {
+ if (e.session_id) st.session = e.session_id;
+ if (e.total_cost_usd != null) st.cost = (st.cost || 0) + Number(e.total_cost_usd);
+ const u = e.usage || {};
+ const tokens = Number(u.input_tokens || u.inputTokens || 0) + Number(u.output_tokens || u.outputTokens || 0);
+ finalEv(st, e.result, { isError: !!e.is_error, ms: e.duration_ms, turns: e.num_turns, cost: e.total_cost_usd, tokens: tokens || null }, line, ts);
+ }
+}
+
+/**
+ * Parse a stream that is already in memory.
+ *
+ * @param {string} text the jsonl feed
+ * @returns {object} the state: { events, session, model, cwd, usage, cost, line }
+ */
+function parseText(text) {
+ const st = freshState();
+ for (const l of String(text || '').split('\n')) {
+ st.line++;
+ if (!l || l[0] !== '{') continue;
+ let e; try { e = JSON.parse(l); } catch { continue; }
+ try { handle(st, e, st.line); } catch { /* one odd event never breaks the view */ }
+ }
+ return st;
+}
+
+/**
+ * Parse what was added to the file since the last call; returns the cached state.
+ *
+ * @param {string} file the jsonl feed
+ * @returns {object} the state
+ */
+function parse(file) {
+ const entry = cache.get(file);
+ let st = entry && entry.state;
+ let size = 0;
+ try { size = fs.statSync(file).size; } catch { forget(file); return freshState(); }
+ if (!st || size < st.offset) { forget(file); st = freshState(); }
+ if (size === st.offset) {
+ if (entry && entry.state === st) { cache.delete(file); cache.set(file, entry); }
+ return st;
+ }
+ const fd = fs.openSync(file, 'r');
+ try {
+ // Read in chunks; stop at the last complete line.
+ while (st.offset < size) {
+ const len = Math.min(size - st.offset, 4 * 1024 * 1024);
+ const buf = Buffer.alloc(len);
+ fs.readSync(fd, buf, 0, len, st.offset);
+ const end = buf.lastIndexOf(10);
+ if (end < 0) {
+ if (len === size - st.offset) break; // the last line is still being written
+ st.offset += len; // a single line over 4 MB: skip it rather than stall
+ continue;
+ }
+ for (const l of buf.subarray(0, end).toString('utf8').split('\n')) {
+ st.line++;
+ if (!l || l[0] !== '{') continue;
+ let e; try { e = JSON.parse(l); } catch { continue; }
+ try { handle(st, e, st.line); } catch { /* one odd event never breaks the view */ }
+ }
+ st.offset += end + 1;
+ }
+ } finally { fs.closeSync(fd); }
+ remember(file, st);
+ return st;
+}
+
+/**
+ * The events from index `from`, plus where the client should ask from next time (the first tool still waiting).
+ *
+ * @param {object} st the state
+ * @param {number} [from] the first event wanted
+ * @param {number} [limit] how many events at most
+ * @returns {object} { events, from, total, skipped, next, info }
+ */
+function view(st, from = 0, limit = 600) {
+ const total = st.events.length;
+ const open = st.events.findIndex((e) => e.t === 'tool' && e.pending);
+ let start = Math.max(0, Math.min(from, total));
+ let skipped = 0;
+ if (start === 0 && total > limit) { skipped = total - limit; start = skipped; }
+ return {
+ events: st.events.slice(start),
+ from: start, total, skipped,
+ next: open >= 0 ? Math.min(open, total) : total,
+ info: { session: st.session, model: st.model, cwd: st.cwd, usage: st.usage, cost: st.cost },
+ };
+}
+
+// The few numbers a list of jobs needs from a log, memoised by the file's size and mtime: an unchanged log is never
+// parsed again (the full parse cache above holds 60 files, and a busy day has hundreds). persistStats(file) keeps the
+// memo across restarts.
+const STATS = new Map();
+let statsFile = null, statsDirty = false;
+
+/** The numbers of a parsed stream: tools, edits, errors, tokens, cost. */
+function statsOf(st) {
+ const u = st.usage || { input: 0, output: 0, cached: 0 };
+ let tools = 0, edits = 0, errs = 0, lastTool = '', lastErr = '';
+ for (const e of st.events) {
+ if (e.t === 'tool') { tools++; if (e.kind === 'edit') edits++; if (e.isError) errs++; lastTool = `${e.name} ${e.summary}`; }
+ if (e.t === 'error' || (e.t === 'final' && e.isError && e.text)) lastErr = String(e.text).slice(0, 200);
+ }
+ return {
+ tools, edits, errs, cost: Number.isFinite(st.cost) ? Number(st.cost) : null,
+ input: u.input || 0, output: u.output || 0, cached: u.cached || 0,
+ model: st.model || null, lastTool: lastTool.slice(0, 200), lastErr,
+ };
+}
+
+/** The numbers of a log, memoised by the file's size and mtime. */
+function stats(file) {
+ let stt; try { stt = fs.statSync(file); } catch { return null; }
+ const hit = STATS.get(file);
+ if (hit && hit.size === stt.size && hit.mtime === stt.mtimeMs) return hit.s;
+ const s = statsOf(parse(file));
+ STATS.set(file, { size: stt.size, mtime: stt.mtimeMs, s });
+ statsDirty = true;
+ return s;
+}
+
+/** Keep the memo across restarts, in a file of the caller's choosing. */
+function persistStats(file) {
+ statsFile = file;
+ try { for (const [k, v] of Object.entries(JSON.parse(fs.readFileSync(file, 'utf8')))) STATS.set(k, v); } catch { /* first run */ }
+ setInterval(() => {
+ if (!statsDirty || !statsFile) return;
+ statsDirty = false;
+ for (const k of STATS.keys()) if (!fs.existsSync(k)) STATS.delete(k);
+ try { fs.writeFileSync(`${statsFile}.tmp`, JSON.stringify(Object.fromEntries(STATS))); fs.renameSync(`${statsFile}.tmp`, statsFile); } catch { statsDirty = true; }
+ }, 30000).unref();
+}
+
+module.exports = { parse, parseText, view, summary, kindOf, textOf, clip, stats, statsOf, persistStats, atOf };
new file mode 100644
new file mode 100644
@@ -0,0 +1,159 @@
@@ -0,0 +1,159 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.codes/schemas/engine/transcript@1",
+ "title": "OpenVibe.Codes transcript",
+ "description": "One event shape for every harness's log: Claude Code stream-json, Codex, Command Code, OpenCode, dsh --json and MiniMax Code stream-json. The normaliser lives in engine/transcript.js.",
+ "type": "object",
+ "required": ["events"],
+ "properties": {
+ "events": {
+ "type": "array",
+ "description": "The conversation, in order.",
+ "items": { "$ref": "#/$defs/event" }
+ },
+ "session": {
+ "type": ["string", "null"],
+ "description": "The harness's session id, when it reported one (a continue needs it)."
+ },
+ "model": {
+ "type": ["string", "null"],
+ "description": "The model that worked on it, when the feed said."
+ },
+ "cwd": {
+ "type": ["string", "null"],
+ "description": "The folder it worked in."
+ },
+ "usage": {
+ "type": "object",
+ "description": "The tokens it used.",
+ "properties": {
+ "input": { "type": "number", "minimum": 0 },
+ "output": { "type": "number", "minimum": 0 },
+ "cached": { "type": "number", "minimum": 0 }
+ },
+ "additionalProperties": false
+ },
+ "cost": {
+ "type": ["number", "null"],
+ "description": "The dollars the feed reported, when it reported any."
+ },
+ "line": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "The number of feed lines that were read."
+ }
+ },
+ "additionalProperties": true,
+ "$defs": {
+ "event": {
+ "type": "object",
+ "required": ["t"],
+ "description": "One event of the conversation.",
+ "properties": {
+ "t": {
+ "type": "string",
+ "enum": ["user", "text", "thinking", "tool", "error", "final", "meta"],
+ "description": "The kind of event."
+ },
+ "text": {
+ "type": "string",
+ "description": "The words: what was said, thought, or went wrong."
+ },
+ "name": {
+ "type": "string",
+ "description": "The tool's name (on a tool event)."
+ },
+ "kind": {
+ "type": "string",
+ "enum": ["shell", "read", "edit", "search", "web", "plan", "tool"],
+ "description": "What kind of tool it was (on a tool event)."
+ },
+ "summary": {
+ "type": "string",
+ "description": "One line about what the tool was asked to do."
+ },
+ "input": {
+ "type": "object",
+ "description": "What the tool was given.",
+ "properties": {
+ "text": { "type": "string" },
+ "more": { "type": "number", "minimum": 0 }
+ },
+ "required": ["text"],
+ "additionalProperties": true
+ },
+ "output": {
+ "type": ["object", "null"],
+ "description": "What the tool answered, once it did.",
+ "properties": {
+ "text": { "type": "string" },
+ "more": { "type": "number", "minimum": 0 }
+ },
+ "required": ["text"],
+ "additionalProperties": true
+ },
+ "at": {
+ "type": ["string", "null"],
+ "format": "date-time",
+ "description": "When it happened (ISO 8601), when the feed said."
+ },
+ "ts": {
+ "type": ["number", "null"],
+ "description": "When it happened, in milliseconds since the epoch."
+ },
+ "line": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "The feed line it came from."
+ },
+ "i": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Its index in the event list."
+ },
+ "id": {
+ "type": "string",
+ "description": "The tool call's id (on a tool event)."
+ },
+ "pending": {
+ "type": "boolean",
+ "description": "The tool has not answered yet."
+ },
+ "isError": {
+ "type": "boolean",
+ "description": "It failed."
+ },
+ "exit": {
+ "type": "number",
+ "description": "The exit code of the command (on a tool event)."
+ },
+ "v": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "How often this event changed."
+ },
+ "partial": {
+ "type": "boolean",
+ "description": "The words are still streaming in."
+ },
+ "answer": {
+ "type": "boolean",
+ "description": "This message is the answer of the turn."
+ },
+ "session": {
+ "type": "string",
+ "description": "The session id (on a meta event)."
+ },
+ "model": {
+ "type": "string",
+ "description": "The model (on a meta event)."
+ },
+ "cwd": {
+ "type": "string",
+ "description": "The folder (on a meta event)."
+ }
+ },
+ "additionalProperties": true
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
+'use strict';
+// The per-task budget, ported from the portal's jobbudget.js: steps, minutes and dollars, and the wrap-up turn that
+// lands the work when the steps or the minutes run out. Dollars stay a hard stop.
+
+const assert = require('node:assert/strict');
+const { check, done } = require('./helpers/boot');
+const { limitsFor, evaluate, state, wrapUp, wrapUpPrompt, STATES } = require('../engine/budget');
+
+(async () => {
+ await check('the limits are the config\'s, then the runner\'s, then the task\'s', () => {
+ const cfg = { budget: { steps: 100, minutes: 60, usd: 5, perRunner: { cc: { steps: 50 } } } };
+ assert.deepEqual(limitsFor(cfg, 'mm'), { steps: 100, minutes: 60, usd: 5 });
+ assert.deepEqual(limitsFor(cfg, 'cc'), { steps: 50, minutes: 60, usd: 5 });
+ assert.deepEqual(limitsFor(cfg, 'mm', { steps: 10, usd: 1 }), { steps: 10, minutes: 60, usd: 1 });
+ assert.deepEqual(limitsFor({}, 'mm'), { steps: null, minutes: null, usd: null });
+ });
+
+ await check('a limit that is not set is no limit, never a zero budget', () => {
+ assert.deepEqual(limitsFor({ budget: { steps: 0, minutes: -1, usd: 'many' } }, 'mm'), { steps: null, minutes: null, usd: null });
+ assert.equal(evaluate({ steps: 100000 }, limitsFor({}, 'mm')).hit, null);
+ });
+
+ await check('the first limit reached wins', () => {
+ const limits = { steps: 100, minutes: 60, usd: 5 };
+ assert.equal(evaluate({ steps: 120, minutes: 90, usd: 9 }, limits).hit, 'steps');
+ assert.equal(evaluate({ steps: 10, minutes: 90, usd: 9 }, limits).hit, 'minutes');
+ assert.equal(evaluate({ steps: 10, minutes: 20, usd: 9 }, limits).hit, 'usd');
+ assert.equal(evaluate({ steps: 10, minutes: 20, usd: 1 }, limits).hit, null);
+ });
+
+ await check('an unknown spend is not a reason to stop', () => {
+ assert.equal(evaluate({ steps: 1, minutes: null, usd: null }, { steps: 100, minutes: 60, usd: 5 }).hit, null);
+ const out = evaluate({ steps: 1, minutes: null, usd: null }, { steps: 100, minutes: 60, usd: 5 });
+ assert.equal(out.used.minutes, null);
+ assert.equal(out.used.usd, null);
+ });
+
+ await check('a task that stopped on a limit is budget-stopped, never a vague failure', () => {
+ assert.equal(state('budget-stopped'), 'budget-stopped');
+ assert.equal(state('failed'), 'failed');
+ assert.equal(state('interrupted'), 'failed');
+ assert.equal(state('done'), 'done');
+ assert.equal(state('running'), 'running');
+ assert.ok(STATES.includes('budget-stopped'));
+ });
+
+ await check('steps and minutes buy one wrap-up turn', () => {
+ const steps = wrapUp('steps');
+ assert.equal(steps.action, 'wrap-up');
+ assert.match(steps.prompt, /one short last turn/);
+ assert.match(steps.prompt, /Do not start anything new/);
+ assert.equal(wrapUp('minutes').action, 'wrap-up');
+ });
+
+ await check('a dollar limit is a hard stop', () => {
+ const dollars = wrapUp('usd');
+ assert.equal(dollars.action, 'stop');
+ assert.equal(dollars.prompt, null);
+ });
+
+ await check('only one wrap-up turn', () => {
+ assert.equal(wrapUp('steps', { done: true }).action, 'stop');
+ });
+
+ await check('the wrap-up prompt says which limit was reached', () => {
+ assert.match(wrapUpPrompt('minutes'), /your minutes limit/);
+ });
+
+ done();
+})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,162 @@
@@ -0,0 +1,162 @@
+'use strict';
+// The chain of one task, ported from the portal's test-chain-end.js: a failed job's line followed one hop, and a
+// step-limit stop with nothing after it was never attention. The chain is followed to the run that ended it: through
+// retry links, through a session that carried a run on (its parent), and through what resolved the next hop.
+//
+// The portal read those links from marker files in its log folder; the engine is pure, so the links come in with the
+// attempts.
+
+const assert = require('node:assert/strict');
+const { check, done } = require('./helpers/boot');
+const chain = require('../engine/chain');
+
+const now = Date.now();
+const job = (runner, name, status, at, extra = {}) => ({
+ key: `${runner}:${name}`, runner, name, runnerName: runner.toUpperCase(),
+ status, updated: at, error: status === 'done' ? '' : 'it broke', ...extra,
+});
+const carried = (parent, name, at) => ({
+ key: `cd:${name}`, runner: 'mm', harness: 'mm', parent, runnerName: 'MiniMax Code',
+ name: `↻ ${name}`, status: 'done', updated: at, error: '', session: true,
+});
+
+(async () => {
+ await check('a chain is followed through its retries and the sessions that carried it on', () => {
+ const attempts = [
+ // a: crashed, its retry stopped at the step limit, and a session carried that retry on to the end.
+ job('cc', 'a', 'failed', now - 9000, { next: 'mm:a-r2' }),
+ job('mm', 'a-r2', 'budget-stopped', now - 5000),
+ carried('mm:a-r2', 'a-r2', now - 1000),
+ // b: crashed, its retry stopped at the step limit, and nothing carried it on.
+ job('cx', 'b', 'failed', now - 9000, { next: 'mm:b-r2' }),
+ job('mm', 'b-r2', 'budget-stopped', now - 5000),
+ // c: the same as b, but the owner marked the stop handled.
+ job('cc', 'c', 'failed', now - 9000, { next: 'mm:c-r2' }),
+ job('mm', 'c-r2', 'budget-stopped', now - 5000, { resolvedNote: 'finished by hand in PR 12' }),
+ // d: crashed, its retry crashed too, and the second retry finished.
+ job('cc', 'd', 'failed', now - 9000, { next: 'mm:d-r2' }),
+ job('mm', 'd-r2', 'failed', now - 5000, { next: 'cm:d-r3' }),
+ job('cm', 'd-r3', 'done', now - 1000),
+ ];
+ const runs = chain.annotate(attempts);
+ const by = Object.fromEntries(runs.map((r) => [r.key, r]));
+
+ assert.equal(by['cc:a'].resolved.how, 'done', 'a session that carried the retry on finished the chain');
+ assert.equal(by['cc:a'].resolved.byName, '↻ a-r2');
+ assert.equal(by['mm:a-r2'].resolved.how, 'done', 'the step-limit stop it carried on is resolved too');
+
+ assert.equal(by['cx:b'].resolved.how, 'budget', 'the chain ended in a step-limit stop');
+ assert.match(by['cx:b'].resolved.note, /b-r2, which stopped at its step limit before it finished/);
+ assert.doesNotMatch(by['cx:b'].resolved.note, /failed again/, 'a step-limit stop is not called a failure');
+ assert.equal(by['mm:b-r2'].resolved, undefined, 'the unfinished stop needs attention');
+ assert.equal(by['mm:b-r2'].reason, 'budget');
+
+ assert.equal(by['mm:c-r2'].resolved.how, 'note', 'a stop the owner marked handled is handled');
+ assert.equal(by['cc:c'].resolved.how, 'note', 'and so is the job it carried on');
+ assert.match(by['cc:c'].resolved.note, /finished by hand in PR 12/);
+
+ assert.equal(by['cc:d'].resolved.how, 'done', 'two hops down, the chain finished');
+ assert.equal(by['cc:d'].resolved.byName, 'd-r3');
+ assert.equal(by['mm:d-r2'].resolved.how, 'done');
+ });
+
+ await check('a handled note that names where the work went is followed to how that job ended', () => {
+ const attempts = [
+ job('cc', 'e', 'failed', now - 9000, { next: 'mm:e-r2' }),
+ job('mm', 'e-r2', 'budget-stopped', now - 5000, { resolvedNote: 'Superseded: the commits are being integrated by mm:integrate-x.' }),
+ job('mm', 'integrate-x', 'budget-stopped', now - 3000),
+ carried('mm:integrate-x', 'integrate-x', now - 1000),
+ ];
+ const m = Object.fromEntries(chain.annotate(attempts).map((r) => [r.key, r]));
+ assert.equal(m['mm:e-r2'].resolved.how, 'done', 'the note named the job that finished the work');
+ assert.equal(m['mm:e-r2'].resolved.byName, '↻ integrate-x');
+ assert.match(m['mm:e-r2'].resolved.said, /being integrated by mm:integrate-x/, 'the note is kept');
+ assert.equal(m['cc:e'].resolved.how, 'done');
+ });
+
+ await check('the attempts are not changed by annotating them', () => {
+ const attempts = [job('cc', 'a', 'failed', now - 9000, { next: 'mm:a-r2' }), job('mm', 'a-r2', 'budget-stopped', now - 5000)];
+ const before = JSON.stringify(attempts);
+ chain.annotate(attempts);
+ assert.equal(JSON.stringify(attempts), before, 'the input is never changed');
+ });
+
+ await check('the outcome of a task is its state, its reason and its next step', () => {
+ const attempts = [
+ job('cc', 'a', 'failed', now - 9000, { error: 'usage limit reached' }),
+ job('mm', 'a-r2', 'done', now - 5000),
+ ];
+ const out = chain.outcome(attempts);
+ assert.equal(out.state, 'done');
+ assert.equal(out.end.key, 'mm:a-r2');
+ assert.equal(out.step.action, 'done');
+ assert.equal(out.attempts.length, 2);
+ });
+
+ await check('a limit hands the task to another runner', () => {
+ const cfg = { subagents: { cc: { enabled: true, max: 2 }, mm: { enabled: true, max: 2 } }, fallback: { order: ['cc', 'mm'] } };
+ const ctx = { cfg, health: { cc: { state: 'ok' }, mm: { state: 'ok' } }, counts: {}, models: {}, runners: ['cc', 'mm'] };
+ const attempts = [job('cc', 'a', 'failed', now, { error: 'usage limit reached' })];
+ const step = chain.nextStep(attempts, ctx);
+ assert.equal(step.action, 'switch');
+ assert.equal(step.runner, 'mm');
+ assert.match(step.why, /hit its usage limit/);
+ });
+
+ await check('a real error is retried on another runner, one reasoning level up', () => {
+ const cfg = { subagents: { cc: { enabled: true, max: 2 }, mm: { enabled: true, max: 2 } }, fallback: { order: ['cc', 'mm'] } };
+ const ctx = { cfg, health: { cc: { state: 'ok' }, mm: { state: 'ok' } }, counts: {}, models: {}, runners: ['cc', 'mm'] };
+ const attempts = [job('cc', 'a', 'failed', now, { error: 'the tests did not pass', effort: 'medium' })];
+ const step = chain.nextStep(attempts, ctx);
+ assert.equal(step.action, 'switch');
+ assert.equal(step.runner, 'mm');
+ assert.equal(step.effort, 'high', 'one level up: the same attempt again rarely helps');
+ });
+
+ await check('a crash is continued in the same session', () => {
+ const attempts = [{ ...job('cc', 'a', 'interrupted', now), facts: { session: 'sess-1', exit: 137 } }];
+ const step = chain.nextStep(attempts);
+ assert.equal(step.action, 'continue');
+ assert.equal(step.session, 'sess-1');
+ assert.match(step.why, /crash/);
+ });
+
+ await check('a context that grew too big starts fresh instead of continuing', () => {
+ const attempts = [{ ...job('cc', 'a', 'failed', now), facts: { apiError: 'prompt is too long: 250000 tokens' } }];
+ const step = chain.nextStep(attempts);
+ assert.equal(step.action, 'restart');
+ assert.match(step.why, /context grew too big/);
+ });
+
+ await check('a stop is not a failure and is never handed to another runner', () => {
+ const attempts = [job('cc', 'a', 'stopped', now)];
+ const step = chain.nextStep(attempts, { cfg: { subagents: {}, fallback: { order: [] } } });
+ assert.equal(step.action, 'stop');
+ });
+
+ await check('a step limit buys one wrap-up turn, a dollar limit does not', () => {
+ const steps = [job('cc', 'a', 'budget-stopped', now, { budget: { hit: 'steps' } })];
+ assert.equal(chain.nextStep(steps).action, 'wrap-up');
+ assert.match(chain.nextStep(steps).prompt, /one short last turn/);
+ const dollars = [job('cc', 'a', 'budget-stopped', now, { budget: { hit: 'usd' } })];
+ assert.equal(chain.nextStep(dollars).action, 'stop');
+ const done = [job('cc', 'a', 'budget-stopped', now, { budget: { hit: 'steps' }, wrapUp: true })];
+ assert.equal(chain.nextStep(done).action, 'stop', 'only one wrap-up turn');
+ });
+
+ await check('a running task is waited for, not decided', () => {
+ assert.equal(chain.nextStep([job('cc', 'a', 'running', now)]).action, 'wait');
+ assert.equal(chain.nextStep([job('cc', 'a', 'done', now)]).action, 'done');
+ });
+
+ await check('the names of one job, its chain and its base are told apart', () => {
+ assert.equal(chain.base('bench-7-finish'), 'bench7');
+ assert.equal(chain.base('bench7-r2'), 'bench7');
+ assert.equal(chain.chainRoot('x-r2'), 'x');
+ assert.equal(chain.chainRoot('x-c1'), 'x');
+ assert.equal(chain.sameChain('x', 'x-b'), true);
+ assert.equal(chain.sameChain('seo-batch-a', 'seo-batch-b'), false);
+ });
+
+ done();
+})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,86 @@
@@ -0,0 +1,86 @@
+'use strict';
+// The reasoning level a harness accepts, ported from the portal's test-effort-fit.js. The 2026-10-29 incident: a job
+// retried on another runner copied cx's "medium" to Command Code, which died before its first step ("Unknown effort
+// \"medium\". Supported: off, low, high, max."). Levels are also uneven (off and ultra exist on one harness only), so
+// the ordering must know them all or a cross-runner value passes through untouched.
+//
+// The blocked-model list, from the portal's test-blocked-models.js: a model the provider refuses is replaced by one it
+// still lists, and a launch with nothing left is an error, never a silent run with a refused model.
+
+const assert = require('node:assert/strict');
+const { check, done } = require('./helpers/boot');
+const { fitEffort, stepUp, EFFORTS, pickModel, blockModel, unblockModel, isBlocked, harnessOf } = require('../engine/effort');
+
+const ALL = ['off', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'];
+
+(async () => {
+ await check('every level a harness offers is a level the ordering knows', () => {
+ for (const [harness, list] of Object.entries(EFFORTS)) {
+ for (const level of list) assert.ok(ALL.includes(level), `${harness} offers ${level}, which fitEffort does not order`);
+ }
+ });
+
+ await check('whatever comes in, the result is accepted by the harness', () => {
+ for (const [harness, list] of Object.entries(EFFORTS)) {
+ for (const level of ALL) {
+ const got = fitEffort(harness, level);
+ if (list.length) assert.ok(list.includes(got), `${harness} would be handed ${got} for ${level}`);
+ else assert.equal(got, level, `${harness} has no effort setting: ${level} passes through`);
+ }
+ }
+ });
+
+ await check('the reported case and the neighbours it must not break', () => {
+ assert.equal(fitEffort('cc', 'medium'), 'high');
+ assert.equal(fitEffort('cc', 'xhigh'), 'max');
+ assert.equal(fitEffort('cc', 'ultra'), 'max');
+ assert.equal(fitEffort('cc', 'off'), 'off');
+ assert.equal(fitEffort('cx', 'off'), 'low');
+ assert.equal(fitEffort('dsh', 'medium'), 'high');
+ assert.equal(fitEffort('cc', 'high'), 'high', 'an accepted level is left alone');
+ assert.equal(fitEffort('cc', null), null);
+ });
+
+ await check('Codes\' harness names are the same harnesses', () => {
+ assert.equal(harnessOf('command-code'), 'cc');
+ assert.equal(harnessOf('codex'), 'cx');
+ assert.equal(harnessOf('opencode'), 'oc');
+ assert.equal(harnessOf('claude-code'), 'claude');
+ assert.equal(harnessOf('deepseek'), 'dsh');
+ assert.equal(fitEffort('command-code', 'medium'), 'high');
+ assert.equal(fitEffort('claude-code', 'high'), 'high');
+ });
+
+ await check('a retry goes one level up', () => {
+ assert.equal(stepUp('medium', EFFORTS.cc), 'high');
+ assert.equal(stepUp('max', EFFORTS.cc), 'max');
+ assert.equal(stepUp('medium', EFFORTS.cx), 'high');
+ });
+
+ await check('a blocked model is replaced by one the harness still lists', () => {
+ const blockedModels = { cx: ['gpt-6.1-sol', 'gpt-6.1-terra'] };
+ const models = { cx: ['gpt-6.1-sol', 'gpt-6-sol'] };
+ assert.equal(pickModel({ runner: 'cx', model: 'gpt-6.1-sol', models, blockedModels }), 'gpt-6-sol');
+ assert.equal(pickModel({ runner: 'cx', model: 'gpt-6.1-terra', models, blockedModels }), 'gpt-6-sol');
+ assert.equal(pickModel({ runner: 'cx', model: 'gpt-6-sol', models, blockedModels }), 'gpt-6-sol', 'an unblocked model is left alone');
+ assert.equal(pickModel({ runner: 'cx', model: null, models, blockedModels }), null, 'no model asked for, none forced');
+ });
+
+ await check('a launch with nothing unblocked left is an error', () => {
+ const blockedModels = { cx: ['gpt-6.1-sol', 'gpt-6-sol'] };
+ const models = { cx: ['gpt-6.1-sol', 'gpt-6-sol'] };
+ assert.throws(() => pickModel({ runner: 'cx', model: 'gpt-6.1-sol', models, blockedModels }), /No unblocked model/);
+ });
+
+ await check('a model is blocked only when the provider says it does not have it', () => {
+ assert.deepEqual(blockModel([], 'gpt-6.1-sol', 'model_not_found'), ['gpt-6.1-sol']);
+ assert.deepEqual(blockModel([], 'gpt-6.1-sol', 'not available on your plan'), ['gpt-6.1-sol']);
+ assert.deepEqual(blockModel([], 'gpt-6.1-sol', 'the connection was reset'), [], 'a crash is not a missing model');
+ assert.deepEqual(blockModel(['gpt-6.1-sol'], 'gpt-6.1-sol', 'model_not_found'), ['gpt-6.1-sol'], 'no twice');
+ assert.deepEqual(unblockModel(['a', 'b'], 'a'), ['b']);
+ assert.equal(isBlocked({ cx: ['x'] }, 'cx', 'x'), true);
+ assert.equal(isBlocked({ cx: ['x'] }, 'cx', 'y'), false);
+ });
+
+ done();
+})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
+'use strict';
+// The failure reasons, ported from the portal's test-failure-reason.js: why a run failed, and what that means for the
+// routing. A limit, a lost sign-in, a busy model and a runner turned off are the runner switch's business; a real
+// error is not (and must not be mistaken for one).
+
+const assert = require('node:assert/strict');
+const { check, done } = require('./helpers/boot');
+const { reason, SWITCHABLE } = require('../engine/chain');
+const { track } = require('../engine/router');
+
+const generic = 'API Error: Command Code ended without a result (credits, rate limit or a crash: check its exit code)';
+const CASES = [
+ [generic, 'error'],
+ [generic + '\nAPI Error: usage limit reached', 'limit'],
+ ['mcode exec failed: The run failed: Internal server error. Please retry your request. (500). Retry after the connection recovers.', 'capacity'],
+ ['HTTP 502 Bad Gateway', 'capacity'], ['status code: 504', 'capacity'],
+ ['Request rejected (429)', 'limit'], ['HTTP 429', 'limit'],
+ ['API key has expired', 'auth'], ['model at capacity', 'capacity'],
+ ['turned off in the agent portal', 'refused'],
+ ['AssertionError: expected 500 items, got 429', 'error'],
+ ['test failed: expected HTTP 200, got 404', 'error'],
+];
+
+(async () => {
+ await check('every error is classified', () => {
+ for (const [text, expected] of CASES) assert.equal(reason(text), expected, text);
+ assert.equal(reason(''), 'unknown');
+ });
+
+ await check('a provider outage is the runner switch\'s business', () => {
+ assert.equal(SWITCHABLE.has(reason(CASES[2][0])), true, 'provider outages use existing fallback');
+ });
+
+ await check('an unknown failure is not excluded as a quota failure', () => {
+ assert.deepEqual(track([{ runner: 'cc', status: 'failed', updated: Date.now(), reason: reason(generic) }]), { cc: { done: 0, failed: 1 } }, 'unknown failures must not be excluded as quota failures');
+ });
+
+ await check('a provider outage does not penalise a runner\'s reliability', () => {
+ assert.deepEqual(track([{ runner: 'mm', status: 'failed', updated: Date.now(), reason: reason(CASES[2][0]) }]), {}, 'provider outages do not penalize coding reliability');
+ });
+
+ done();
+})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
+'use strict';
+// The engine as a package: what it exports, and what it may depend on.
+
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const { check, done } = require('./helpers/boot');
+const engine = require('../engine');
+
+const DIR = path.join(__dirname, '..', 'engine');
+
+(async () => {
+ await check('the engine exports every module and the names a caller wants', () => {
+ for (const name of ['router', 'chain', 'budget', 'transcript', 'effort', 'scope', 'runerror']) {
+ assert.ok(engine[name], `${name} is exported`);
+ }
+ for (const name of ['classify', 'pick', 'outcome', 'nextStep', 'evaluate', 'limitsFor', 'parse', 'parseText', 'fitEffort', 'pickModel']) {
+ assert.equal(typeof engine[name], 'function', `${name} is a function`);
+ }
+ });
+
+ await check('the engine needs nothing but node itself', () => {
+ const builtins = new Set(require('node:module').builtinModules);
+ for (const file of fs.readdirSync(DIR)) {
+ if (!file.endsWith('.js')) continue;
+ const source = fs.readFileSync(path.join(DIR, file), 'utf8');
+ for (const m of source.matchAll(/require\('([^']+)'\)/g)) {
+ const name = m[1];
+ if (name.startsWith('.')) continue;
+ assert.ok(builtins.has(name.replace(/^node:/, '')), `${file} requires ${name}, which is not a node builtin`);
+ }
+ }
+ });
+
+ await check('every module of the engine has a file and a test', () => {
+ const files = fs.readdirSync(DIR).filter((f) => f.endsWith('.js')).map((f) => f.replace(/\.js$/, ''));
+ const tests = fs.readdirSync(__dirname).filter((f) => f.startsWith('engine-') && f.endsWith('.test.js'))
+ .map((f) => f.replace(/^engine-/, '').replace(/\.test\.js$/, ''));
+ for (const name of files) {
+ assert.ok(tests.includes(name.replace(/-/g, '-')), `${name} has a test`);
+ }
+ });
+
+ done();
+})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,113 @@
@@ -0,0 +1,113 @@
+'use strict';
+// The tier router, ported from the portal's test-router-brief.js and test-router-outcomes.js.
+//
+// Bug 3: the router judges a job by its launch prompt. A prompt that only says "read /…/x.brief.md and follow it" was
+// routed as "a short question" to a free model, so a hard job ran on a cheap one. The router now classifies on the
+// brief the prompt names (and on the job's own brief), and honours the tier line of that brief.
+
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+const { check, done } = require('./helpers/boot');
+const router = require('../engine/router');
+
+const folder = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-router-'));
+const JOBS = path.join(folder, 'jobs');
+fs.mkdirSync(JOBS);
+
+(async () => {
+ try {
+ const hard = 'Root cause of the race condition in the payment webhook.\n\n'.repeat(40) + '\ntier: hard\n';
+ fs.writeFileSync(path.join(JOBS, 'pin-round.brief.md'), hard);
+ const pointer = `Read ${path.join(JOBS, 'pin-round.brief.md')} first and follow it exactly.`;
+
+ await check('the prompt alone is judged as it reads', () => {
+ assert.equal(router.classify(pointer).tier, 'basic');
+ assert.equal(router.classify(pointer).why, 'a short question');
+ });
+
+ await check('the brief a prompt names decides the tier', () => {
+ const judged = router.classify(pointer, { JOBS, name: 'pin-round' });
+ assert.equal(judged.tier, 'hard', 'the brief decides the tier');
+ assert.equal(judged.brief, path.join(JOBS, 'pin-round.brief.md'));
+ });
+
+ await check('the tier line of the brief is honoured wherever it stands in it', () => {
+ fs.writeFileSync(path.join(JOBS, 'chore.brief.md'), 'Rename a variable in one file.\n\ntier: basic\n');
+ assert.equal(router.classify(`Follow ${path.join(JOBS, 'chore.brief.md')}`, { JOBS, name: 'chore' }).tier, 'basic');
+ });
+
+ await check("the job's own brief counts too, when the prompt does not name a file", () => {
+ assert.equal(router.classify('Do it.', { JOBS, name: 'pin-round' }).tier, 'hard');
+ assert.equal(router.classify('Do it.', { JOBS, name: 'unknown-job' }).tier, 'basic', 'a job without a brief is judged by its prompt');
+ });
+
+ await check('a prompt that names a file that is not there falls back to the prompt', () => {
+ assert.equal(router.classify('Read /nowhere/jobs/missing.brief.md and follow it.', { JOBS, name: 'nothing' }).tier, 'basic');
+ });
+
+ await check('the pick follows the brief: a hard job does not go to the free runner', () => {
+ const cfg = { subagents: { mm: { enabled: true, max: 2 }, cc: { enabled: false, max: 0 } } };
+ const ctx = { cfg, health: { mm: { state: 'ok' } }, counts: {}, models: {}, runs: [] };
+ assert.equal(router.pick(pointer, { ...ctx, JOBS, name: 'pin-round' }).runner, 'mm');
+ assert.equal(router.pick(pointer, { ...ctx, JOBS, name: 'pin-round' }).tier, 'hard');
+ assert.ok(router.pick(pointer, { ...ctx, JOBS, name: 'pin-round' }).why.includes('the brief it names'));
+ });
+
+ await check('the tiers are the five the product knows', () => {
+ assert.deepEqual(router.TIERS, ['basic', 'research', 'standard', 'hard', 'design']);
+ for (const tier of router.TIERS) assert.ok(router.LADDER[tier].length, `${tier} has a ladder`);
+ });
+
+ await check('a rescued failure does not turn into a success', () => {
+ const now = Date.now();
+ const run = (runner, status, extra = {}) => ({ runner, status, updated: now, ...extra });
+ const runs = [
+ run('mm', 'failed', { reason: 'error', resolved: { how: 'done', by: 'dsh:rescue' } }),
+ run('mm', 'failed', { reason: 'error', resolved: { how: 'running', by: 'mm:retry' } }),
+ run('mm', 'interrupted', { resolved: { how: 'note', note: 'Superseded' } }),
+ run('mm', 'done'),
+ run('dsh', 'done'),
+ ];
+ assert.deepEqual(router.track(runs), { mm: { done: 1, failed: 3 }, dsh: { done: 1, failed: 0 } });
+ for (const reason of ['limit', 'auth', 'capacity', 'refused']) {
+ assert.deepEqual(router.track([run('mm', 'failed', { reason })]), {}, reason);
+ }
+ assert.deepEqual(router.track([
+ run('mm', 'running'), run('mm', 'queued'), run('mm', 'stopped'),
+ run('mm', 'done', { updated: now - 8 * 86400e3 }),
+ run('mm', 'done', { session: true }), run('ship', 'done'),
+ ]), {});
+ });
+
+ await check('a runner that mostly fails is skipped, until it does better', () => {
+ const now = Date.now();
+ const run = (runner, status, extra = {}) => ({ runner, status, updated: now, ...extra });
+ const runs = [
+ run('mm', 'failed', { reason: 'error' }), run('mm', 'failed', { reason: 'error' }),
+ run('mm', 'failed', { reason: 'error' }), run('mm', 'done'),
+ run('dsh', 'done'),
+ ];
+ const context = {
+ cfg: { subagents: { mm: { enabled: true, max: 2 }, dsh: { enabled: true, max: 2 } } },
+ health: { mm: { state: 'ok' }, dsh: { state: 'ok' } }, counts: {}, models: {}, runs,
+ };
+ assert.equal(router.pick('tier: standard\nFix the parser', context).runner, 'dsh', 'rescued failures must not bypass reliability gate');
+ assert.equal(router.pick('tier: standard\nFix the parser', { ...context, runs: runs.slice(0, 3) }).runner, 'mm', 'fewer than four attempts retain the existing sampling rule');
+ assert.equal(router.pick('tier: standard\nFix the parser', { ...context, runs: [...runs, run('mm', 'done')] }).runner, 'mm', 'two successes in five attempts meet the existing threshold');
+ });
+
+ await check('a runner that is off, unhealthy or full is not chosen', () => {
+ const cfg = { subagents: { mm: { enabled: true, max: 2 }, cx: { enabled: true, max: 2 } } };
+ const off = { cfg, health: { mm: { state: 'ok' }, cx: { state: 'ok' } }, counts: {}, models: {}, runs: [] };
+ assert.equal(router.pick('tier: standard\nFix the parser', { ...off, cfg: { subagents: { mm: { enabled: false, max: 2 }, cx: { enabled: true, max: 2 } } } }).runner, 'cx');
+ assert.equal(router.pick('tier: standard\nFix the parser', { ...off, health: { mm: { state: 'login' }, cx: { state: 'ok' } } }).runner, 'cx');
+ assert.equal(router.pick('tier: standard\nFix the parser', { ...off, counts: { cx: 2 } }).runner, 'mm');
+ assert.equal(router.pick('tier: standard\nFix the parser', { ...off, cfg: { subagents: { mm: { enabled: true, max: 2 }, cx: { enabled: true, max: 2 } } }, health: { mm: { state: 'login' }, cx: { state: 'login' } } }), null);
+ });
+ } finally {
+ fs.rmSync(folder, { recursive: true, force: true });
+ }
+ done();
+})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
+'use strict';
+// What a failed run really said, ported from the portal's test-runerror.js: the wrapper's own no-result message is
+// possible causes, not an observed failure; the launch command echoed after a killed child is never the error.
+
+const assert = require('node:assert/strict');
+const { check, done } = require('./helpers/boot');
+const { failureError } = require('../engine/runerror');
+
+const generic = 'API Error: Command Code ended without a result (credits, rate limit or a crash: check its exit code)';
+const event = (result) => JSON.stringify({ type: 'result', is_error: true, result });
+
+(async () => {
+ await check('the error of a failed run is told from the feed, the stderr and the summary', () => {
+ assert.equal(failureError(event('API Error: model "x" is unavailable\nRetry later.'), 'launcher noise'), 'API Error: model "x" is unavailable\nRetry later.');
+ assert.equal(failureError(event(generic), '/r/cc-run.sh: line 61: 998 Killed cmd -p "# brief\n- report PRs --yolo -m model\n"'), 'Killed');
+ assert.equal(failureError(event(generic), '/r/cc-run.sh: line 61: 998 Killed cmd -p "# brief\nAPI Error: quota in a quoted example\n"'), 'Killed');
+ assert.equal(failureError(event(generic), 'Terminated.\n- report PRs --yolo'), 'Terminated.');
+ assert.equal(failureError(event(generic), '- report PRs --yolo'), generic);
+ assert.equal(failureError(event(generic), 'API Error: usage limit reached'), 'API Error: usage limit reached');
+ assert.equal(failureError('partial JSON\n' + event('Refused: off'), ''), 'Refused: off');
+ assert.equal(failureError(JSON.stringify({ type: 'tool_result', text: 'Error: test fixture' }), '', 'actual provider failure'), 'actual provider failure');
+ assert.equal(failureError(JSON.stringify({ type: 'result', is_error: true, errors: ['one', { message: 'two' }] }), ''), 'one; two');
+ assert.equal(failureError('', '[claude-code:unrecognized_model] {"model":"example"}'), '[claude-code:unrecognized_model] {"model":"example"}');
+ assert.equal(failureError('', 'mcode exec failed: Internal server error. (500).'), 'mcode exec failed: Internal server error. (500).');
+ });
+
+ await check('the error is short and never empty by accident', () => {
+ assert.equal(failureError(event('x'.repeat(500)), '').length, 200);
+ assert.equal(failureError('', ''), '');
+ });
+
+ done();
+})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
+'use strict';
+// The job scopes, ported from the portal's jobscope.js. On a machine without systemd (or with the scopes turned off)
+// the module must do nothing at all: the command is spawned as it was, and the reaper has nothing to reap.
+
+const assert = require('node:assert/strict');
+const { check, done } = require('./helpers/boot');
+
+process.env.OV_NO_JOB_SCOPES = '1';
+const scope = require('../engine/scope');
+
+(async () => {
+ await check('without systemd the command is spawned as it was', () => {
+ assert.equal(scope.available(), false);
+ const wrapped = scope.wrap('node', ['server.js'], 'cc-my-job');
+ assert.equal(wrapped.cmd, 'node');
+ assert.deepEqual(wrapped.args, ['server.js']);
+ assert.equal(wrapped.unit, null);
+ });
+
+ await check('without systemd the reaper has nothing to reap', async () => {
+ assert.deepEqual(await scope.reap(), []);
+ assert.deepEqual(await scope.reap({ log: () => { throw new Error('the reaper must stay quiet'); } }), []);
+ assert.equal(await scope.stopFor(1234), null);
+ });
+
+ await check('the slice and the per-job caps are the portal\'s', () => {
+ assert.equal(scope.SLICE, 'ovjobs.slice');
+ });
+
+ done();
+})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,146 @@
@@ -0,0 +1,146 @@
+'use strict';
+// The transcript normaliser, ported from the portal's test-transcript.js (the cache: retention, eviction, replay,
+// append, truncation and deletion) and extended with the event shape every harness ends up in.
+
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+const vm = require('node:vm');
+const { check, done } = require('./helpers/boot');
+const transcript = require('../engine/transcript');
+
+const folder = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-transcript-'));
+const source = fs.readFileSync(path.join(__dirname, '..', 'engine', 'transcript.js'), 'utf8');
+
+/** The module again, with a smaller cache, so the eviction can be seen. */
+function parser(budget) {
+ const context = { require, module: { exports: {} }, Buffer };
+ vm.runInNewContext(source.replace('32 * 1024 * 1024', String(budget)), context);
+ return context.module.exports;
+}
+
+function log(name, text) {
+ const file = path.join(folder, name);
+ fs.writeFileSync(file, JSON.stringify({ type: 'result', result: text }) + '\n');
+ return file;
+}
+
+(async () => {
+ try {
+ await check('the cache keeps, evicts, replays, appends, truncates and forgets', () => {
+ const small = parser(4096);
+ const first = log('first', 'first answer');
+ const firstState = small.parse(first);
+ assert.equal(small.parse(first), firstState);
+ const second = log('second', 'second answer');
+ const secondState = small.parse(second);
+ assert.equal(small.parse(first), firstState);
+ small.parse(log('third', 'third answer'));
+ assert.equal(small.parse(first), firstState);
+ const reloaded = small.parse(second);
+ assert.notEqual(reloaded, secondState);
+ assert.equal(reloaded.events[0].text, 'second answer');
+ const oversized = log('large', 'large answer '.repeat(1000));
+ const largeState = small.parse(oversized);
+ assert.notEqual(small.parse(oversized), largeState);
+ assert.equal(small.parse(oversized).events[0].text, largeState.events[0].text);
+ fs.appendFileSync(first, JSON.stringify({ type: 'result', result: 'appended' }) + '\n');
+ assert.equal(small.parse(first).events.at(-1).text, 'appended');
+ fs.writeFileSync(first, JSON.stringify({ type: 'result', result: 'new' }) + '\n');
+ assert.equal(small.parse(first).events.length, 1);
+ assert.equal(small.parse(first).events[0].text, 'new');
+ fs.unlinkSync(first);
+ assert.equal(small.parse(first).events.length, 0);
+ const many = parser(1024 * 1024);
+ const oldest = log('oldest', 'oldest');
+ const original = many.parse(oldest);
+ for (let index = 0; index < 60; index++) many.parse(log(`entry-${index}`, String(index)));
+ assert.notEqual(many.parse(oldest), original);
+ });
+
+ await check('Claude Code stream-json becomes the one shape', () => {
+ const state = transcript.parseText([
+ JSON.stringify({ type: 'system', subtype: 'init', session_id: 'sess-1', model: 'claude-sonnet-5-5', cwd: '/repo' }),
+ JSON.stringify({ type: 'assistant', message: { model: 'claude-sonnet-5-5', content: [{ type: 'text', text: 'On it.' }] } }),
+ JSON.stringify({ type: 'assistant', message: { content: [{ type: 'tool_use', id: 't1', name: 'Bash', input: { command: 'npm test' } }] } }),
+ JSON.stringify({ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 't1', content: 'ok' }] } }),
+ JSON.stringify({ type: 'result', result: 'Done.', session_id: 'sess-1', total_cost_usd: 0.12 }),
+ ].join('\n'));
+ assert.equal(state.session, 'sess-1');
+ assert.equal(state.model, 'claude-sonnet-5-5');
+ assert.equal(state.cost, 0.12);
+ const kinds = state.events.map((e) => e.t);
+ assert.deepEqual(kinds, ['meta', 'text', 'tool', 'final']);
+ const tool = state.events.find((e) => e.t === 'tool');
+ assert.equal(tool.name, 'Bash');
+ assert.equal(tool.kind, 'shell');
+ assert.match(tool.input.text, /npm test/, 'the input is kept as text');
+ assert.equal(tool.pending, false, 'the result arrived');
+ assert.equal(tool.output.text, 'ok');
+ assert.equal(state.events.at(-1).text, 'Done.');
+ });
+
+ await check('a Codex and Command Code feed (the same shape) becomes the one shape', () => {
+ const state = transcript.parseText([
+ JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'Working.' }] } }),
+ JSON.stringify({ type: 'assistant', message: { content: [{ type: 'tool_use', id: 'c1', name: 'shell', input: { command: '/bin/bash -lc "ls -la"' } }] } }),
+ ].join('\n'));
+ const tool = state.events.find((e) => e.t === 'tool');
+ assert.equal(tool.name, 'shell');
+ assert.equal(tool.summary, 'ls -la', 'the wrapped command is unwrapped');
+ });
+
+ await check('DeepSeek\'s dsh --json becomes the one shape', () => {
+ const state = transcript.parseText([
+ JSON.stringify({ type: 'session', sessionId: 'dsh-1', cwd: '/repo' }),
+ JSON.stringify({ type: 'thinking', text: 'hmm' }),
+ JSON.stringify({ type: 'tool_call', callId: 'd1', tool: 'read_file', input: { path: '/repo/a.js' } }),
+ JSON.stringify({ type: 'tool_result', callId: 'd1', result: 'contents', status: 'completed' }),
+ JSON.stringify({ type: 'text', text: 'Read it.' }),
+ ].join('\n'));
+ assert.equal(state.session, 'dsh-1');
+ assert.deepEqual(state.events.map((e) => e.t), ['meta', 'thinking', 'tool', 'text']);
+ const tool = state.events.find((e) => e.t === 'tool');
+ assert.equal(tool.name, 'read_file');
+ assert.equal(tool.kind, 'read');
+ assert.equal(tool.output.text, 'contents');
+ });
+
+ await check('MiniMax Code\'s stream-json becomes the one shape', () => {
+ const state = transcript.parseText([
+ JSON.stringify({ schemaVersion: 1, type: 'item.started', sessionId: 'mm-1', item: { id: 'm1', type: 'agent_message' } }),
+ JSON.stringify({ schemaVersion: 1, type: 'item.completed', item: { id: 'm1', type: 'agent_message', content: 'Hello.' } }),
+ JSON.stringify({ schemaVersion: 1, type: 'item.started', item: { id: 't1', type: 'tool_call', toolCall: { id: 'tc1', name: 'Bash', input: { command: 'npm test' } } } }),
+ JSON.stringify({ schemaVersion: 1, type: 'item.completed', item: { id: 't1', type: 'tool_call', toolCall: { id: 'tc1', name: 'Bash', output: 'ok' } } }),
+ JSON.stringify({ schemaVersion: 1, type: 'exec.completed', result: { answer: 'All good.', status: 'succeeded' } }),
+ ].join('\n'));
+ assert.equal(state.session, 'mm-1');
+ assert.deepEqual(state.events.map((e) => e.t), ['text', 'tool', 'final']);
+ const tool = state.events.find((e) => e.t === 'tool');
+ assert.equal(tool.name, 'Bash');
+ assert.equal(tool.output.text, 'ok');
+ assert.equal(state.events.at(-1).text, 'All good.');
+ });
+
+ await check('every event says when it happened', () => {
+ const state = transcript.parseText(JSON.stringify({ type: 'result', result: 'Done.', timestampMs: 1712345678900 }));
+ assert.equal(state.events[0].at, '2024-04-05T19:34:38.900Z');
+ assert.equal(transcript.atOf(null), null);
+ assert.equal(transcript.atOf(1712345678900), '2024-04-05T19:34:38.900Z');
+ });
+
+ await check('the schema of the shape is published and describes an event', () => {
+ const schema = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'engine', 'transcript.schema.json'), 'utf8'));
+ assert.equal(schema.$id, 'https://openvibe.codes/schemas/engine/transcript@1');
+ const event = schema.$defs.event;
+ for (const field of ['t', 'text', 'name', 'input', 'output', 'at']) {
+ assert.ok(event.properties[field], `the schema describes ${field}`);
+ }
+ assert.deepEqual(event.properties.t.enum, ['user', 'text', 'thinking', 'tool', 'error', 'final', 'meta']);
+ });
+ } finally {
+ fs.rmSync(folder, { recursive: true, force: true });
+ }
+ done();
+})();