scenri 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/serve.js
CHANGED
|
@@ -8,7 +8,7 @@ import Database from 'better-sqlite3';
|
|
|
8
8
|
import { randomBytes, createHash, randomUUID, timingSafeEqual } from 'crypto';
|
|
9
9
|
import fs, { realpathSync, existsSync, readFileSync, mkdirSync, chmodSync, writeFileSync, readdirSync, statSync, rmSync, renameSync } from 'fs';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
|
-
import { readFile, copyFile,
|
|
11
|
+
import { readFile, copyFile, mkdtemp, rm, readdir, stat, writeFile } from 'fs/promises';
|
|
12
12
|
import { spawn } from 'child_process';
|
|
13
13
|
import sharp6 from 'sharp';
|
|
14
14
|
import Fastify from 'fastify';
|
|
@@ -1850,59 +1850,175 @@ function createFalEngine(opts) {
|
|
|
1850
1850
|
}
|
|
1851
1851
|
};
|
|
1852
1852
|
}
|
|
1853
|
+
|
|
1854
|
+
// ../engines/codex/src/locate.ts
|
|
1855
|
+
var MIN_CODEX_VERSION = "0.145.0";
|
|
1856
|
+
var WHERE_TIMEOUT_MS = 5e3;
|
|
1857
|
+
function parseCodexVersion(text) {
|
|
1858
|
+
const m = /codex-cli\s+(\d+\.\d+\.\d+)/.exec(text);
|
|
1859
|
+
return m ? m[1] : null;
|
|
1860
|
+
}
|
|
1861
|
+
function versionAtLeast(version, floor) {
|
|
1862
|
+
const a = version.split(".").map(Number);
|
|
1863
|
+
const b = floor.split(".").map(Number);
|
|
1864
|
+
for (let i = 0; i < 3; i++) {
|
|
1865
|
+
if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0);
|
|
1866
|
+
}
|
|
1867
|
+
return true;
|
|
1868
|
+
}
|
|
1869
|
+
function resolveCodex(platform, spawnImpl, timeoutMs = WHERE_TIMEOUT_MS) {
|
|
1870
|
+
if (platform !== "win32") return Promise.resolve({ command: "codex", direct: true });
|
|
1871
|
+
const fallback = { command: "codex", direct: false };
|
|
1872
|
+
return new Promise((resolve) => {
|
|
1873
|
+
let settled = false;
|
|
1874
|
+
const done = (r) => {
|
|
1875
|
+
if (settled) return;
|
|
1876
|
+
settled = true;
|
|
1877
|
+
clearTimeout(timer);
|
|
1878
|
+
resolve(r);
|
|
1879
|
+
};
|
|
1880
|
+
let child;
|
|
1881
|
+
const timer = setTimeout(() => {
|
|
1882
|
+
done(fallback);
|
|
1883
|
+
child?.kill();
|
|
1884
|
+
}, timeoutMs);
|
|
1885
|
+
try {
|
|
1886
|
+
child = spawnImpl("where.exe", ["codex"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
1887
|
+
} catch {
|
|
1888
|
+
done(fallback);
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
let stdout = "";
|
|
1892
|
+
child.stdout?.on("data", (d) => {
|
|
1893
|
+
stdout += String(d);
|
|
1894
|
+
});
|
|
1895
|
+
child.on("error", () => done(fallback));
|
|
1896
|
+
child.on("exit", (code) => {
|
|
1897
|
+
if (code !== 0) {
|
|
1898
|
+
done(fallback);
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
const hits = stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
1902
|
+
const exe = hits.find((h) => h.toLowerCase().endsWith(".exe"));
|
|
1903
|
+
done(exe ? { command: exe, direct: true } : fallback);
|
|
1904
|
+
});
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// ../engines/codex/src/run.ts
|
|
1853
1909
|
var NOT_INSTALLED_REASON = "Codex CLI is not installed on this computer";
|
|
1854
1910
|
var NOT_AUTHENTICATED_REASON = "Codex CLI is installed but not signed in";
|
|
1911
|
+
var UNVERIFIED_REASON = "Could not verify Codex on this computer";
|
|
1855
1912
|
var DEFAULT_TIMEOUT_MS2 = 3e5;
|
|
1856
|
-
|
|
1913
|
+
var PROBE_TIMEOUT_MS = 1e4;
|
|
1914
|
+
var NO_ACTIVITY_TIMEOUT_MS = 12e4;
|
|
1915
|
+
var PROBE_TTL_MS = 3e4;
|
|
1916
|
+
function execArgs(dir, effort = "low") {
|
|
1857
1917
|
return [
|
|
1858
1918
|
"exec",
|
|
1859
1919
|
"--skip-git-repo-check",
|
|
1860
1920
|
"--sandbox",
|
|
1861
1921
|
"workspace-write",
|
|
1922
|
+
"--color",
|
|
1923
|
+
"never",
|
|
1862
1924
|
"-c",
|
|
1863
1925
|
`model_reasoning_effort="${effort}"`,
|
|
1864
1926
|
"-C",
|
|
1865
1927
|
dir,
|
|
1866
|
-
|
|
1928
|
+
"-"
|
|
1867
1929
|
];
|
|
1868
1930
|
}
|
|
1931
|
+
function killTree(child, platform, spawnImpl) {
|
|
1932
|
+
if (platform === "win32" && child.pid) {
|
|
1933
|
+
try {
|
|
1934
|
+
const tk = spawnImpl("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
1935
|
+
const fallback = () => {
|
|
1936
|
+
try {
|
|
1937
|
+
child.kill();
|
|
1938
|
+
} catch {
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1941
|
+
tk.on("error", fallback);
|
|
1942
|
+
tk.on("exit", (code) => {
|
|
1943
|
+
if (code !== 0) fallback();
|
|
1944
|
+
});
|
|
1945
|
+
return;
|
|
1946
|
+
} catch {
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
try {
|
|
1950
|
+
child.kill();
|
|
1951
|
+
} catch {
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1869
1954
|
function createRunner(opts = {}) {
|
|
1870
1955
|
const spawnImpl = opts.spawnImpl ?? spawn;
|
|
1871
1956
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
1957
|
+
const probeTimeoutMs = opts.probeTimeoutMs ?? PROBE_TIMEOUT_MS;
|
|
1958
|
+
const probeTtlMs = opts.probeTtlMs ?? PROBE_TTL_MS;
|
|
1959
|
+
const noActivityMs = opts.noActivityMs ?? NO_ACTIVITY_TIMEOUT_MS;
|
|
1872
1960
|
const platform = opts.platform ?? process.platform;
|
|
1961
|
+
const killCodex = (child) => killTree(child, platform, spawnImpl);
|
|
1962
|
+
let resolved = null;
|
|
1963
|
+
async function resolution() {
|
|
1964
|
+
resolved ??= await resolveCodex(platform, spawnImpl);
|
|
1965
|
+
return resolved;
|
|
1966
|
+
}
|
|
1873
1967
|
const winArg = (a) => `"${a.replace(/[\r\n]+/g, " ").replace(/"/g, "'").replace(/%/g, " percent ")}"`;
|
|
1874
|
-
const spawnCodex = (
|
|
1875
|
-
stdio
|
|
1876
|
-
shell: true
|
|
1877
|
-
}
|
|
1878
|
-
function run2(args, signal) {
|
|
1968
|
+
const spawnCodex = (exe, args, stdinOpen) => {
|
|
1969
|
+
const stdio = [stdinOpen ? "pipe" : "ignore", "pipe", "pipe"];
|
|
1970
|
+
return exe.direct ? spawnImpl(exe.command, args, { stdio }) : spawnImpl([exe.command, ...args.map(winArg)].join(" "), [], { stdio, shell: true });
|
|
1971
|
+
};
|
|
1972
|
+
async function run2(args, signal, io) {
|
|
1973
|
+
const exe = await resolution();
|
|
1879
1974
|
return new Promise((resolve, reject) => {
|
|
1880
1975
|
let child;
|
|
1881
1976
|
try {
|
|
1882
|
-
child = spawnCodex(args);
|
|
1977
|
+
child = spawnCodex(exe, args, io?.stdin != null);
|
|
1883
1978
|
} catch (err) {
|
|
1884
1979
|
reject(new Error(`Failed to spawn codex: ${err.message}`));
|
|
1885
1980
|
return;
|
|
1886
1981
|
}
|
|
1887
1982
|
let settled = false;
|
|
1888
1983
|
let stderr = "";
|
|
1889
|
-
|
|
1890
|
-
|
|
1984
|
+
let activityTimer = setTimeout(onSilence, noActivityMs);
|
|
1985
|
+
function sawActivity() {
|
|
1986
|
+
if (settled) return;
|
|
1987
|
+
clearTimeout(activityTimer);
|
|
1988
|
+
activityTimer = setTimeout(onSilence, noActivityMs);
|
|
1989
|
+
}
|
|
1990
|
+
function onSilence() {
|
|
1991
|
+
finish(
|
|
1992
|
+
() => reject(
|
|
1993
|
+
new Error(`Codex CLI produced no output for ${Math.round(noActivityMs / 1e3)}s, treating it as stuck`)
|
|
1994
|
+
)
|
|
1995
|
+
);
|
|
1996
|
+
killCodex(child);
|
|
1997
|
+
}
|
|
1998
|
+
child.stdout?.on("data", sawActivity);
|
|
1891
1999
|
child.stderr?.on("data", (d) => {
|
|
1892
2000
|
stderr += String(d);
|
|
2001
|
+
sawActivity();
|
|
1893
2002
|
});
|
|
2003
|
+
if (io?.stdin != null) {
|
|
2004
|
+
child.stdin?.on("error", () => {
|
|
2005
|
+
});
|
|
2006
|
+
child.stdin?.write(io.stdin);
|
|
2007
|
+
child.stdin?.end();
|
|
2008
|
+
}
|
|
1894
2009
|
const timer = setTimeout(() => {
|
|
1895
|
-
child.kill();
|
|
1896
2010
|
finish(() => reject(new Error(`Codex CLI timed out after ${timeoutMs}ms`)));
|
|
2011
|
+
killCodex(child);
|
|
1897
2012
|
}, timeoutMs);
|
|
1898
2013
|
const onAbort = () => {
|
|
1899
|
-
child.kill();
|
|
1900
2014
|
finish(() => reject(new Error("Codex CLI run aborted")));
|
|
2015
|
+
killCodex(child);
|
|
1901
2016
|
};
|
|
1902
2017
|
function finish(fn) {
|
|
1903
2018
|
if (settled) return;
|
|
1904
2019
|
settled = true;
|
|
1905
2020
|
clearTimeout(timer);
|
|
2021
|
+
clearTimeout(activityTimer);
|
|
1906
2022
|
signal?.removeEventListener("abort", onAbort);
|
|
1907
2023
|
fn();
|
|
1908
2024
|
}
|
|
@@ -1937,47 +2053,99 @@ function createRunner(opts = {}) {
|
|
|
1937
2053
|
});
|
|
1938
2054
|
}
|
|
1939
2055
|
}
|
|
1940
|
-
function
|
|
2056
|
+
function probeSpawn(exe, args) {
|
|
2057
|
+
const CAP = 8192;
|
|
1941
2058
|
return new Promise((resolve) => {
|
|
1942
2059
|
let settled = false;
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
2060
|
+
let stdout = "";
|
|
2061
|
+
const done = (outcome) => {
|
|
2062
|
+
if (settled) return;
|
|
2063
|
+
settled = true;
|
|
2064
|
+
clearTimeout(timer);
|
|
2065
|
+
resolve({ outcome, stdout });
|
|
1948
2066
|
};
|
|
1949
2067
|
let child;
|
|
2068
|
+
const timer = setTimeout(() => {
|
|
2069
|
+
done("timeout");
|
|
2070
|
+
if (child) killCodex(child);
|
|
2071
|
+
}, probeTimeoutMs);
|
|
1950
2072
|
try {
|
|
1951
|
-
child = spawnCodex(args);
|
|
2073
|
+
child = spawnCodex(exe, args, false);
|
|
1952
2074
|
} catch {
|
|
1953
|
-
done(
|
|
2075
|
+
done("spawn-error");
|
|
1954
2076
|
return;
|
|
1955
2077
|
}
|
|
1956
|
-
child.stdout?.on("data", () => {
|
|
2078
|
+
child.stdout?.on("data", (d) => {
|
|
2079
|
+
if (stdout.length < CAP) stdout += String(d).slice(0, CAP - stdout.length);
|
|
1957
2080
|
});
|
|
1958
2081
|
child.stderr?.on("data", () => {
|
|
1959
2082
|
});
|
|
1960
|
-
child.on("error", () => done(
|
|
1961
|
-
child.on("exit", (code) => done(code === 0));
|
|
2083
|
+
child.on("error", () => done("spawn-error"));
|
|
2084
|
+
child.on("exit", (code) => done(code === 0 ? "ok" : "nonzero"));
|
|
1962
2085
|
});
|
|
1963
2086
|
}
|
|
2087
|
+
let lastDiag = "";
|
|
2088
|
+
function verdict(avail, exe, version) {
|
|
2089
|
+
const line = `codex probe: exe=${exe.command} version=${version ?? "unknown"} outcome=${avail.ok ? "ready" : avail.code}`;
|
|
2090
|
+
if (line !== lastDiag) {
|
|
2091
|
+
lastDiag = line;
|
|
2092
|
+
if (!avail.ok) console.warn(line);
|
|
2093
|
+
else if (process.env.SCENRI_DEBUG === "1") console.log(line);
|
|
2094
|
+
}
|
|
2095
|
+
return avail;
|
|
2096
|
+
}
|
|
2097
|
+
let cached = null;
|
|
1964
2098
|
async function probe() {
|
|
1965
2099
|
if (process.env.SCENRI_NO_CODEX === "1") {
|
|
1966
2100
|
return { ok: false, reason: NOT_INSTALLED_REASON, code: "not-installed" };
|
|
1967
2101
|
}
|
|
1968
|
-
if (
|
|
1969
|
-
return
|
|
2102
|
+
if (cached && probeTtlMs > 0 && Date.now() - cached.at < probeTtlMs) {
|
|
2103
|
+
return cached.value;
|
|
1970
2104
|
}
|
|
1971
|
-
|
|
1972
|
-
|
|
2105
|
+
const value = await probeUncached();
|
|
2106
|
+
cached = { at: Date.now(), value };
|
|
2107
|
+
return value;
|
|
2108
|
+
}
|
|
2109
|
+
async function probeUncached() {
|
|
2110
|
+
resolved = await resolveCodex(platform, spawnImpl);
|
|
2111
|
+
const exe = resolved;
|
|
2112
|
+
const ver = await probeSpawn(exe, ["--version"]);
|
|
2113
|
+
if (ver.outcome === "timeout") {
|
|
2114
|
+
return verdict({ ok: false, reason: UNVERIFIED_REASON, code: "unverified" }, exe, null);
|
|
1973
2115
|
}
|
|
1974
|
-
|
|
2116
|
+
if (ver.outcome !== "ok") {
|
|
2117
|
+
return verdict({ ok: false, reason: NOT_INSTALLED_REASON, code: "not-installed" }, exe, null);
|
|
2118
|
+
}
|
|
2119
|
+
const version = parseCodexVersion(ver.stdout);
|
|
2120
|
+
if (version && !versionAtLeast(version, MIN_CODEX_VERSION)) {
|
|
2121
|
+
return verdict(
|
|
2122
|
+
{
|
|
2123
|
+
ok: false,
|
|
2124
|
+
reason: `Codex CLI ${version} is too old. Scenri needs ${MIN_CODEX_VERSION} or newer.`,
|
|
2125
|
+
code: "update-needed"
|
|
2126
|
+
},
|
|
2127
|
+
exe,
|
|
2128
|
+
version
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
2131
|
+
const login = await probeSpawn(exe, ["login", "status"]);
|
|
2132
|
+
if (login.outcome === "ok") {
|
|
2133
|
+
return verdict({ ok: true }, exe, version);
|
|
2134
|
+
}
|
|
2135
|
+
if (login.outcome === "nonzero") {
|
|
2136
|
+
return verdict({ ok: false, reason: NOT_AUTHENTICATED_REASON, code: "not-authenticated" }, exe, version);
|
|
2137
|
+
}
|
|
2138
|
+
return verdict({ ok: false, reason: UNVERIFIED_REASON, code: "unverified" }, exe, version);
|
|
1975
2139
|
}
|
|
1976
|
-
|
|
2140
|
+
function invalidateProbe() {
|
|
2141
|
+
cached = null;
|
|
2142
|
+
resolved = null;
|
|
2143
|
+
}
|
|
2144
|
+
return { run: run2, withWorkDir, probe, invalidateProbe };
|
|
1977
2145
|
}
|
|
1978
2146
|
var OUT_FILE = "analysis.json";
|
|
1979
2147
|
function createCodexAnalyzer(opts = {}) {
|
|
1980
|
-
const runner = createRunner(opts);
|
|
2148
|
+
const runner = opts.runner ?? createRunner(opts);
|
|
1981
2149
|
return {
|
|
1982
2150
|
isAvailable: () => runner.probe(),
|
|
1983
2151
|
async analyze(req, signal) {
|
|
@@ -1990,11 +2158,11 @@ function createCodexAnalyzer(opts = {}) {
|
|
|
1990
2158
|
}
|
|
1991
2159
|
let problems = [];
|
|
1992
2160
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1993
|
-
const args = execArgs(dir,
|
|
2161
|
+
const args = execArgs(dir, "high");
|
|
1994
2162
|
for (const ref of refs) {
|
|
1995
2163
|
args.splice(args.length - 1, 0, `--image=${ref}`);
|
|
1996
2164
|
}
|
|
1997
|
-
await runner.run(args, signal);
|
|
2165
|
+
await runner.run(args, signal, { stdin: buildPrompt(req, refs.length, problems) });
|
|
1998
2166
|
let raw;
|
|
1999
2167
|
try {
|
|
2000
2168
|
raw = await readFile(join(dir, OUT_FILE), "utf8");
|
|
@@ -2122,12 +2290,19 @@ var INSTALL_COMMAND_SUDO = "sudo npm install -g @openai/codex";
|
|
|
2122
2290
|
var DEFAULT_INSTALL_TIMEOUT_MS = 18e4;
|
|
2123
2291
|
function stateFrom(avail) {
|
|
2124
2292
|
if (avail.ok) return "ready";
|
|
2125
|
-
|
|
2293
|
+
switch (avail.code) {
|
|
2294
|
+
case "not-authenticated":
|
|
2295
|
+
case "update-needed":
|
|
2296
|
+
case "unverified":
|
|
2297
|
+
return avail.code;
|
|
2298
|
+
default:
|
|
2299
|
+
return "not-installed";
|
|
2300
|
+
}
|
|
2126
2301
|
}
|
|
2127
2302
|
function createCodexSetup(opts = {}) {
|
|
2128
2303
|
const spawnImpl = opts.spawnImpl ?? spawn;
|
|
2129
2304
|
const platform = opts.platform ?? process.platform;
|
|
2130
|
-
const runner = createRunner(opts);
|
|
2305
|
+
const runner = opts.runner ?? createRunner(opts);
|
|
2131
2306
|
const installTimeoutMs = opts.installTimeoutMs ?? DEFAULT_INSTALL_TIMEOUT_MS;
|
|
2132
2307
|
function run2(cmd, args, timeoutMs) {
|
|
2133
2308
|
return new Promise((resolve) => {
|
|
@@ -2141,8 +2316,8 @@ function createCodexSetup(opts = {}) {
|
|
|
2141
2316
|
};
|
|
2142
2317
|
let child;
|
|
2143
2318
|
const timer = setTimeout(() => {
|
|
2144
|
-
child?.kill();
|
|
2145
2319
|
done({ code: null, stderr, spawnError: `${cmd} timed out after ${timeoutMs}ms` });
|
|
2320
|
+
if (child) killTree(child, platform, spawnImpl);
|
|
2146
2321
|
}, timeoutMs);
|
|
2147
2322
|
try {
|
|
2148
2323
|
child = spawnImpl(cmd, args, { stdio: ["ignore", "pipe", "pipe"], shell: platform === "win32" });
|
|
@@ -2161,12 +2336,15 @@ function createCodexSetup(opts = {}) {
|
|
|
2161
2336
|
}
|
|
2162
2337
|
return {
|
|
2163
2338
|
async status() {
|
|
2339
|
+
runner.invalidateProbe();
|
|
2164
2340
|
const avail = await runner.probe();
|
|
2165
|
-
|
|
2341
|
+
const setupPlatform = platform === "win32" ? "windows" : platform === "darwin" ? "mac" : "linux";
|
|
2342
|
+
return { state: stateFrom(avail), reason: avail.reason, platform: setupPlatform };
|
|
2166
2343
|
},
|
|
2167
2344
|
async install() {
|
|
2168
2345
|
const res = await run2("npm", ["install", "-g", "@openai/codex"], installTimeoutMs);
|
|
2169
2346
|
if (res.code === 0) {
|
|
2347
|
+
runner.invalidateProbe();
|
|
2170
2348
|
const avail = await runner.probe();
|
|
2171
2349
|
if (avail.code === "not-installed") {
|
|
2172
2350
|
return {
|
|
@@ -2191,6 +2369,7 @@ function createCodexSetup(opts = {}) {
|
|
|
2191
2369
|
},
|
|
2192
2370
|
async login() {
|
|
2193
2371
|
const res = await run2("codex", ["login"], installTimeoutMs);
|
|
2372
|
+
runner.invalidateProbe();
|
|
2194
2373
|
if (res.code === 0) return { ok: true };
|
|
2195
2374
|
const detail = (res.spawnError ?? res.stderr).trim().slice(0, 400) || void 0;
|
|
2196
2375
|
return { ok: false, fallbackCommand: "codex login --device-auth", detail };
|
|
@@ -2201,10 +2380,20 @@ function createCodexSetup(opts = {}) {
|
|
|
2201
2380
|
// ../engines/codex/src/index.ts
|
|
2202
2381
|
function createCodexEngine(opts) {
|
|
2203
2382
|
const { saveImage } = opts;
|
|
2204
|
-
const
|
|
2383
|
+
const platform = opts.platform ?? process.platform;
|
|
2384
|
+
const runner = opts.runner ?? createRunner(opts);
|
|
2205
2385
|
const runCodex = runner.run;
|
|
2206
2386
|
const withWorkDir = runner.withWorkDir;
|
|
2207
|
-
|
|
2387
|
+
const generatedImagesDir = () => join(process.env.CODEX_HOME || join(homedir(), ".codex"), "generated_images");
|
|
2388
|
+
async function snapshotGenerated() {
|
|
2389
|
+
if (platform !== "win32") return null;
|
|
2390
|
+
try {
|
|
2391
|
+
return new Set(await readdir(generatedImagesDir()));
|
|
2392
|
+
} catch {
|
|
2393
|
+
return /* @__PURE__ */ new Set();
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
async function collectImages(dir, before = null) {
|
|
2208
2397
|
const entries = await readdir(dir);
|
|
2209
2398
|
const outFiles = entries.filter((name) => /^out-.*\.png$/.test(name)).sort((a, b) => {
|
|
2210
2399
|
const na = Number(/^out-(\d+)\.png$/.exec(a)?.[1] ?? NaN);
|
|
@@ -2213,6 +2402,10 @@ function createCodexEngine(opts) {
|
|
|
2213
2402
|
return a.localeCompare(b);
|
|
2214
2403
|
});
|
|
2215
2404
|
if (outFiles.length === 0) {
|
|
2405
|
+
if (before) {
|
|
2406
|
+
const recovered = await recoverFromGenerated(before);
|
|
2407
|
+
if (recovered) return [recovered];
|
|
2408
|
+
}
|
|
2216
2409
|
throw new Error("Codex finished but produced no images");
|
|
2217
2410
|
}
|
|
2218
2411
|
const hashes = [];
|
|
@@ -2221,6 +2414,21 @@ function createCodexEngine(opts) {
|
|
|
2221
2414
|
}
|
|
2222
2415
|
return hashes;
|
|
2223
2416
|
}
|
|
2417
|
+
async function recoverFromGenerated(before) {
|
|
2418
|
+
const home = generatedImagesDir();
|
|
2419
|
+
let names;
|
|
2420
|
+
try {
|
|
2421
|
+
names = (await readdir(home)).filter((n) => !before.has(n));
|
|
2422
|
+
} catch {
|
|
2423
|
+
return null;
|
|
2424
|
+
}
|
|
2425
|
+
if (!names.length) return null;
|
|
2426
|
+
const stamped = await Promise.all(names.map(async (n) => ({ n, mtime: (await stat(join(home, n))).mtimeMs })));
|
|
2427
|
+
stamped.sort((a, b) => b.mtime - a.mtime);
|
|
2428
|
+
const pick2 = stamped[0].n;
|
|
2429
|
+
console.warn(`codex: workdir empty, recovered ${pick2} from ${home}`);
|
|
2430
|
+
return saveImage(await readFile(join(home, pick2)));
|
|
2431
|
+
}
|
|
2224
2432
|
return {
|
|
2225
2433
|
capabilities() {
|
|
2226
2434
|
return {
|
|
@@ -2250,41 +2458,61 @@ function createCodexEngine(opts) {
|
|
|
2250
2458
|
const count = Math.max(1, req.count);
|
|
2251
2459
|
const refs = req.referenceImages ?? [];
|
|
2252
2460
|
const roles = req.referenceRoles ?? refs.map(() => "reference");
|
|
2461
|
+
const inner = new AbortController();
|
|
2462
|
+
const onOuterAbort = () => inner.abort();
|
|
2463
|
+
if (signal?.aborted) inner.abort();
|
|
2464
|
+
else signal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
2253
2465
|
const jobs = Array.from(
|
|
2254
2466
|
{ length: count },
|
|
2255
2467
|
(_, i) => async () => withWorkDir(async (dir) => {
|
|
2256
|
-
const args = execArgs(dir
|
|
2468
|
+
const args = execArgs(dir);
|
|
2257
2469
|
for (const [idx, ref] of refs.entries()) {
|
|
2258
2470
|
const dest = join(dir, `ref-${idx}.png`);
|
|
2259
2471
|
await copyFile(ref, dest);
|
|
2260
2472
|
args.splice(args.length - 1, 0, `--image=${dest}`);
|
|
2261
2473
|
}
|
|
2262
|
-
await
|
|
2263
|
-
|
|
2474
|
+
const before = await snapshotGenerated();
|
|
2475
|
+
await runCodex(args, inner.signal, { stdin: buildPrompt2(req, i, roles) });
|
|
2476
|
+
return collectImages(dir, before);
|
|
2264
2477
|
})
|
|
2265
2478
|
);
|
|
2266
2479
|
const results = new Array(count);
|
|
2267
2480
|
const failures = [];
|
|
2481
|
+
let fatal = null;
|
|
2268
2482
|
let next = 0;
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2483
|
+
try {
|
|
2484
|
+
const workers = Array.from({ length: Math.min(3, count) }, async () => {
|
|
2485
|
+
while (next < count && !inner.signal.aborted) {
|
|
2486
|
+
const i = next++;
|
|
2487
|
+
try {
|
|
2488
|
+
results[i] = await jobs[i]();
|
|
2489
|
+
} catch (err) {
|
|
2490
|
+
if (signal?.aborted) throw err;
|
|
2491
|
+
results[i] = [];
|
|
2492
|
+
failures.push(err);
|
|
2493
|
+
if (fatal == null && isFatalSetupError(err)) {
|
|
2494
|
+
fatal = err;
|
|
2495
|
+
inner.abort();
|
|
2496
|
+
runner.invalidateProbe();
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2278
2499
|
}
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2500
|
+
});
|
|
2501
|
+
await Promise.all(workers);
|
|
2502
|
+
} finally {
|
|
2503
|
+
signal?.removeEventListener("abort", onOuterAbort);
|
|
2504
|
+
}
|
|
2505
|
+
const images = results.filter(Boolean).flat();
|
|
2506
|
+
if (!images.length && failures.length) throw fatal ?? failures[0];
|
|
2284
2507
|
if (failures.length) {
|
|
2285
2508
|
console.warn(
|
|
2286
2509
|
`codex: ${failures.length} of ${count} variants failed, keeping ${images.length}: ${String(failures[0]?.message ?? failures[0])}`
|
|
2287
2510
|
);
|
|
2511
|
+
return {
|
|
2512
|
+
images,
|
|
2513
|
+
costUsd: 0,
|
|
2514
|
+
raw: { requested: count, partialFailures: failures.map((f) => String(f?.message ?? f)) }
|
|
2515
|
+
};
|
|
2288
2516
|
}
|
|
2289
2517
|
return { images, costUsd: 0 };
|
|
2290
2518
|
},
|
|
@@ -2301,16 +2529,27 @@ function createCodexEngine(opts) {
|
|
|
2301
2529
|
refLines.push(`${name} shows ${EDIT_REFERENCE_ROLE_DIRECTIVE[role]}`);
|
|
2302
2530
|
}
|
|
2303
2531
|
const promptText = `Edit input.png using your image generation/editing tool: ${req.instruction}.` + (refLines.length ? ` ${refLines.join(". ")}.` : "") + ` Do not browse the web or explore files. Save the result in the current directory as out-1.png (you may run the commands needed to save and resize it). Nothing else.`;
|
|
2304
|
-
const args = execArgs(dir
|
|
2532
|
+
const args = execArgs(dir);
|
|
2305
2533
|
for (const name of ["input.png", ...refLines.map((_, i) => `${editRoles[i] ?? "reference"}-${i + 1}.png`)]) {
|
|
2306
2534
|
args.splice(args.length - 1, 0, `--image=${join(dir, name)}`);
|
|
2307
2535
|
}
|
|
2308
|
-
await
|
|
2309
|
-
|
|
2536
|
+
const before = await snapshotGenerated();
|
|
2537
|
+
try {
|
|
2538
|
+
await runCodex(args, signal, { stdin: promptText });
|
|
2539
|
+
} catch (err) {
|
|
2540
|
+
if (isFatalSetupError(err)) runner.invalidateProbe();
|
|
2541
|
+
throw err;
|
|
2542
|
+
}
|
|
2543
|
+
const images = await collectImages(dir, before);
|
|
2310
2544
|
return { images, costUsd: 0 };
|
|
2311
2545
|
});
|
|
2312
2546
|
}
|
|
2313
2547
|
};
|
|
2548
|
+
function isFatalSetupError(err) {
|
|
2549
|
+
return /failed to spawn|ENOENT|not logged in|login required|401|unauthorized/i.test(
|
|
2550
|
+
String(err?.message ?? err)
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2314
2553
|
function buildPrompt2(req, index, roles) {
|
|
2315
2554
|
const roleDirective = REFERENCE_ROLE_DIRECTIVE;
|
|
2316
2555
|
const refDirectives = roles.map(
|
|
@@ -2326,17 +2565,19 @@ function keyGetter(core, settingKey, envVar) {
|
|
|
2326
2565
|
}
|
|
2327
2566
|
function createEngineRegistry(core, extra = []) {
|
|
2328
2567
|
const saveImage = (buf) => core.images.save(buf);
|
|
2568
|
+
const codexRunner = createRunner();
|
|
2329
2569
|
const adapters = [
|
|
2330
2570
|
createOpenRouterEngine({ getKey: keyGetter(core, "openrouter_api_key", "OPENROUTER_API_KEY"), saveImage }),
|
|
2331
2571
|
createReplicateEngine({ getKey: keyGetter(core, "replicate_api_token", "REPLICATE_API_TOKEN"), saveImage }),
|
|
2332
2572
|
createFalEngine({ getKey: keyGetter(core, "fal_key", "FAL_KEY"), saveImage }),
|
|
2333
|
-
createCodexEngine({ saveImage }),
|
|
2573
|
+
createCodexEngine({ saveImage, runner: codexRunner }),
|
|
2334
2574
|
...extra
|
|
2335
2575
|
];
|
|
2336
2576
|
const byId = new Map(adapters.map((a) => [a.capabilities().id, a]));
|
|
2337
2577
|
return {
|
|
2338
2578
|
all: () => adapters,
|
|
2339
|
-
get: (id) => byId.get(id) ?? null
|
|
2579
|
+
get: (id) => byId.get(id) ?? null,
|
|
2580
|
+
codexRunner
|
|
2340
2581
|
};
|
|
2341
2582
|
}
|
|
2342
2583
|
function createDemoEngine(saveImage) {
|
|
@@ -6153,7 +6394,7 @@ function registerPresenterRoutes(app, deps) {
|
|
|
6153
6394
|
// src/routes/assetBuilds.ts
|
|
6154
6395
|
function registerAssetBuildRoutes(app, deps) {
|
|
6155
6396
|
const { core, engines, scenes, presenters } = deps;
|
|
6156
|
-
const analyzer = deps.analyzer ?? createCodexAnalyzer();
|
|
6397
|
+
const analyzer = deps.analyzer ?? createCodexAnalyzer({ runner: engines.codexRunner });
|
|
6157
6398
|
const buildEngine = async () => {
|
|
6158
6399
|
const ordered = [...engines.all()].sort((a, b) => {
|
|
6159
6400
|
const rank = (e) => e.capabilities().id === "codex-cli" ? 0 : 1;
|
|
@@ -6548,7 +6789,7 @@ function registerProjectRoutes(app, deps) {
|
|
|
6548
6789
|
|
|
6549
6790
|
// src/routes/codexSetup.ts
|
|
6550
6791
|
function registerCodexSetupRoutes(app, deps) {
|
|
6551
|
-
const codexSetup = deps.codexSetup ?? createCodexSetup();
|
|
6792
|
+
const codexSetup = deps.codexSetup ?? createCodexSetup({ runner: deps.codexRunner });
|
|
6552
6793
|
let codexSetupBusy = null;
|
|
6553
6794
|
app.get("/api/engines/codex/status", async () => codexSetup.status());
|
|
6554
6795
|
app.post("/api/engines/codex/install", async (_req, reply) => {
|
|
@@ -6781,6 +7022,32 @@ function registerImageRoutes(app, deps) {
|
|
|
6781
7022
|
|
|
6782
7023
|
// src/release/notes.data.ts
|
|
6783
7024
|
var RELEASES = [
|
|
7025
|
+
{
|
|
7026
|
+
version: "0.4.4",
|
|
7027
|
+
date: "2026-08-23",
|
|
7028
|
+
title: "Codex setup tells the truth, and a stuck generation fails instead of running forever.",
|
|
7029
|
+
sections: [
|
|
7030
|
+
{
|
|
7031
|
+
heading: "Codex",
|
|
7032
|
+
body: 'Setup now verifies more than a binary on the path: it checks the version, the sign-in, and says "could not verify" when it cannot tell, with a Check again button instead of a false Connected. Windows instructions say PowerShell, and Codex installed through npm works again.'
|
|
7033
|
+
},
|
|
7034
|
+
{
|
|
7035
|
+
heading: "Fixes",
|
|
7036
|
+
body: "A generation that goes silent now fails within minutes with a plain reason instead of running on with no news, and Cancel stops the Codex process for real, on Windows too. A signed-out or outdated Codex fails fast with the step that fixes it."
|
|
7037
|
+
}
|
|
7038
|
+
]
|
|
7039
|
+
},
|
|
7040
|
+
{
|
|
7041
|
+
version: "0.4.3",
|
|
7042
|
+
date: "2026-08-23",
|
|
7043
|
+
title: "Controls hold still when you press them.",
|
|
7044
|
+
sections: [
|
|
7045
|
+
{
|
|
7046
|
+
heading: "Fixes",
|
|
7047
|
+
body: "Pressing a button, chip or icon button no longer nudges it down. Controls answer with a change of fill or opacity instead, so nothing shifts under the cursor, and selected rows in Settings no longer re-space their labels when you pick them."
|
|
7048
|
+
}
|
|
7049
|
+
]
|
|
7050
|
+
},
|
|
6784
7051
|
{
|
|
6785
7052
|
version: "0.4.2",
|
|
6786
7053
|
date: "2026-08-23",
|
|
@@ -7482,7 +7749,7 @@ function buildServer(opts) {
|
|
|
7482
7749
|
return { ...rest, referenceCount: referenceImages.length };
|
|
7483
7750
|
});
|
|
7484
7751
|
registerProjectRoutes(app, { core });
|
|
7485
|
-
registerCodexSetupRoutes(app, { codexSetup: opts.codexSetup });
|
|
7752
|
+
registerCodexSetupRoutes(app, { codexSetup: opts.codexSetup, codexRunner: engines.codexRunner });
|
|
7486
7753
|
app.get("/api/engines", async () => {
|
|
7487
7754
|
const list2 = [];
|
|
7488
7755
|
for (const e of engines.all()) {
|
|
@@ -7561,11 +7828,17 @@ function buildServer(opts) {
|
|
|
7561
7828
|
);
|
|
7562
7829
|
}
|
|
7563
7830
|
}
|
|
7831
|
+
const NODE_TIMEOUT_MS = 6e5;
|
|
7564
7832
|
async function runNode(nodeId, engine, estimate, work, expect, post) {
|
|
7565
7833
|
const engineId = engine.capabilities().id;
|
|
7566
7834
|
reserved.set(engineId, (reserved.get(engineId) ?? 0) + estimate);
|
|
7567
7835
|
const ctrl = new AbortController();
|
|
7568
7836
|
runningGenerations.set(nodeId, ctrl);
|
|
7837
|
+
let watchdogFired = false;
|
|
7838
|
+
const watchdog = setTimeout(() => {
|
|
7839
|
+
watchdogFired = true;
|
|
7840
|
+
ctrl.abort();
|
|
7841
|
+
}, opts.nodeTimeoutMs ?? NODE_TIMEOUT_MS);
|
|
7569
7842
|
try {
|
|
7570
7843
|
const result = await work(ctrl.signal);
|
|
7571
7844
|
result.images = await normalizePngs(result.images);
|
|
@@ -7574,9 +7847,11 @@ function buildServer(opts) {
|
|
|
7574
7847
|
core.store.completeNode(nodeId, result);
|
|
7575
7848
|
core.ledger.recordCost(engineId, nodeId, result.costUsd);
|
|
7576
7849
|
} catch (err) {
|
|
7577
|
-
if (
|
|
7850
|
+
if (watchdogFired) core.store.failNode(nodeId, "generation timed out after 10 minutes");
|
|
7851
|
+
else if (ctrl.signal.aborted) core.store.cancelNode(nodeId);
|
|
7578
7852
|
else core.store.failNode(nodeId, String(err?.message ?? err));
|
|
7579
7853
|
} finally {
|
|
7854
|
+
clearTimeout(watchdog);
|
|
7580
7855
|
runningGenerations.delete(nodeId);
|
|
7581
7856
|
const left = (reserved.get(engineId) ?? 0) - estimate;
|
|
7582
7857
|
if (left > 1e-9) reserved.set(engineId, left);
|