Updates
Rolling release / release (push) Failing after 1m49s

This commit is contained in:
2026-09-12 19:36:06 -04:00
parent 9ad09b593c
commit d47e9e260f
30 changed files with 917 additions and 162 deletions
+29 -4
View File
@@ -11,6 +11,9 @@ const CHAR_PER_TOKEN = 3;
const THRESHOLD = 0.68;
const MIN_SUMMARY = 80;
const MIN_COMPACT_MESSAGES = 4;
// Spoken sessions only need the last few exchanges. Prefill cost tracks the
// prompt, so a 32k model must not keep a 20k-token voice transcript.
const VOICE_KEEP_TOKENS = 1024;
const COMPACT_PROMPT =
'Summarize this coding-agent conversation. Use exactly these sections:\n' +
'1. Goal\n' +
@@ -75,19 +78,38 @@ function estimateTokens(messages, tools) {
return conversationTokens(messages) + toolTokens(tools);
}
function historyBudget(ctxSize, tools, attempt, extraTokens = 0) {
function dialogTokens(messages) {
return conversationTokens((messages || []).filter((m) => m && m.role !== 'system'));
}
function voiceKeepTokens(ctxSize) {
const cap = ctxSize > 0 ? Number(ctxSize) : 8192;
return Math.min(VOICE_KEEP_TOKENS, Math.max(480, Math.floor(cap * 0.28)));
}
function historyBudget(ctxSize, tools, attempt, extraTokens = 0, opts = {}) {
const cap = ctxSize > 0 ? Number(ctxSize) : 8192;
const toolTok = toolTokens(tools);
const reserve = Math.max(384, Math.floor(cap * (0.18 + (Number(attempt) || 0) * 0.08)));
return Math.max(240, Math.floor(cap * 0.72) - toolTok - reserve - extraTokens);
let budget = Math.max(240, Math.floor(cap * 0.72) - toolTok - reserve - extraTokens);
if (opts && opts.voice) {
const sysTok = Math.max(0, Number(opts.systemTokens) || 0);
budget = Math.min(budget, Math.max(240, sysTok + voiceKeepTokens(cap) - extraTokens));
}
return Math.max(240, budget);
}
function shouldCompact(messages, tools, ctxSize, extraTokens = 0) {
function shouldCompact(messages, tools, ctxSize, extraTokens = 0, opts = {}) {
const dialog = dialogTokens(messages);
const enough = nonSystemCount(messages) >= MIN_COMPACT_MESSAGES || dialog > 1024;
if (opts && opts.voice) {
return dialog > voiceKeepTokens(ctxSize) && enough;
}
const budget = historyBudget(ctxSize, tools, 0, extraTokens);
const tokens = conversationTokens(messages);
// Large first requests or tool results can overflow before four messages.
// A small greeting must not be discarded merely because schemas are large.
return tokens > budget && (nonSystemCount(messages) >= MIN_COMPACT_MESSAGES || conversationTokens((messages || []).filter((m) => m.role !== 'system')) > 1024);
return tokens > budget && enough;
}
function isOverflowError(err) {
@@ -329,11 +351,14 @@ module.exports = {
THRESHOLD,
MIN_SUMMARY,
MIN_COMPACT_MESSAGES,
VOICE_KEEP_TOKENS,
COMPACT_PROMPT,
VOICE_COMPACT_PROMPT,
conversationTokens,
dialogTokens,
toolTokens,
estimateTokens,
voiceKeepTokens,
historyBudget,
shouldCompact,
nonSystemCount,
+27 -11
View File
@@ -72,6 +72,22 @@ function loadedCtxSize() {
return (loaded && loaded.ctxSize) || 8192;
}
function systemTokens(history) {
if (history && history[0] && history[0].role === 'system') return compaction.conversationTokens([history[0]]);
return 0;
}
function historyCompactOpts(session, toolDefs, ctxSize, sidecarTokens, budget, attempt) {
return {
budgetTokens: compaction.historyBudget(ctxSize, toolDefs, attempt || 0, sidecarTokens, {
voice: !!budget.voice,
systemTokens: systemTokens(session.history),
}),
tools: toolDefs,
voice: !!budget.voice,
};
}
function toolResultCap(budget) {
const ctx = loadedCtxSize();
const voice = !!(budget && budget.voice);
@@ -608,22 +624,20 @@ async function runTurn(ctx) {
const sidecarTokens = compaction.conversationTokens(turnSidecars) + 256;
const beforeUsage = compaction.usage(session.history, toolDefs, ctxSize);
emitLive(emit, session.id, jobId, Object.assign({ type: 'context' }, beforeUsage));
if (compaction.shouldCompact(session.history, toolDefs, ctxSize, sidecarTokens)) {
if (compaction.shouldCompact(session.history, toolDefs, ctxSize, sidecarTokens, { voice: !!budget.voice })) {
emitUpdate(emit, session.id, jobId, {
type: 'compaction',
status: 'start',
method: 'llm',
method: budget.voice ? 'heuristic' : 'llm',
used: beforeUsage.used,
limit: beforeUsage.limit,
pct: beforeUsage.pct,
threshold: beforeUsage.threshold,
});
const compactOpts = {
budgetTokens: compaction.historyBudget(ctxSize, toolDefs, 0, sidecarTokens),
tools: toolDefs,
voice: !!budget.voice,
};
session.history = await compaction.compactWithLlm(session.history, Object.assign({}, compactOpts, {
const compactOpts = historyCompactOpts(session, toolDefs, ctxSize, sidecarTokens, budget, 0);
session.history = budget.voice
? compaction.compact(session.history, compactOpts)
: await compaction.compactWithLlm(session.history, Object.assign({}, compactOpts, {
complete: (opts) => engine.complete(Object.assign({}, opts, {
desktopVision: false,
timeoutMs: budget.completeTimeoutMs,
@@ -643,7 +657,7 @@ async function runTurn(ctx) {
emitUpdate(emit, session.id, jobId, {
type: 'compaction',
status: 'done',
method: 'llm',
method: budget.voice ? 'heuristic' : 'llm',
used: afterUsage.used,
limit: afterUsage.limit,
pct: afterUsage.pct,
@@ -715,7 +729,7 @@ async function runTurn(ctx) {
// The estimate can be lower than the model's tokenizer count.
// Every overflow retry must shrink even an apparently small history.
budgetTokens: Math.min(
compaction.historyBudget(ctxSize, toolDefs, overflowTry + 1, sidecarTokens),
historyCompactOpts(session, toolDefs, ctxSize, sidecarTokens, budget, overflowTry + 1).budgetTokens,
Math.max(1, Math.floor(compaction.conversationTokens(session.history) * 0.85))
),
tools: toolDefs,
@@ -863,7 +877,9 @@ async function runTurn(ctx) {
}
if (stopEarly) break;
}
if (prepared.length) toolBudget.markToolRound(budget);
if (prepared.some((item) => item.name !== 'todo_write' && item.name !== 'update_goal')) {
toolBudget.markToolRound(budget);
}
if (stopEarly) {
return endTurn(emit, session, jobId, tracker, stopEarly);
}
+1 -1
View File
@@ -18,7 +18,7 @@ function fromPayload(payload, origin) {
voice,
maxTurns: num(payload.maxTurns, voice ? 6 : 24),
maxShellCalls: unlimitedShell ? 0 : num(payload.maxShellCalls, voice ? 1 : 0),
maxToolRounds: num(payload.maxToolRounds, voice ? 4 : 0),
maxToolRounds: num(payload.maxToolRounds, voice ? 6 : 0),
completeTimeoutMs: voice ? 45000 : 0,
completeIdleMs: voice ? 10000 : 0,
shellCalls: 0,
+161 -68
View File
@@ -7,8 +7,10 @@
const net = require('../lib/net.js');
const truncate = require('./truncate.js');
const WEB_TIMEOUT_MS = 12000;
const PAGE_TIMEOUT_MS = 20000;
const WEB_TIMEOUT_MS = 3500;
const PAGE_TIMEOUT_MS = 8000;
const SEARCH_BUDGET_MS = 8000;
const ENGINE_TIMEOUT_MS = 3000;
const BROWSER_UA =
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
const GOOGLE_UA =
@@ -34,14 +36,14 @@ const ENGINE_NAMES = [
];
const AUTO_ENGINES = [
'duckduckgo',
'ddg_lite',
'jina',
'bing',
'bing_rss',
'google',
'wikipedia',
'ddg_instant',
'ddg_lite',
'bing_rss',
'duckduckgo',
'bing',
'google',
];
const ENGINE_ALIASES = {
@@ -61,13 +63,64 @@ function abortError(timeoutMs) {
return err;
}
function remainingMs(deadline) {
return Math.max(0, Number(deadline) - Date.now());
}
function timeoutErrorResult(ms, extra) {
return Object.assign({ error: 'timed out after ' + ms + 'ms' }, extra || {});
}
function budgetMs(timeoutMs, fallback, max) {
const fallbackMs = Number(fallback) > 0 ? Number(fallback) : SEARCH_BUDGET_MS;
const cap = Number(max) > 0 ? Number(max) : fallbackMs;
const n = Number(timeoutMs);
if (!(n > 0)) return fallbackMs;
return n > cap ? cap : n;
}
function withDeadline(work, deadline, fallback) {
const left = remainingMs(deadline);
if (left <= 0) return Promise.resolve(typeof fallback === 'function' ? fallback() : fallback);
let timer;
const timeout = new Promise((resolve) => {
timer = setTimeout(() => resolve(typeof fallback === 'function' ? fallback() : fallback), left);
});
return Promise.race([Promise.resolve().then(work), timeout]).finally(() => {
if (timer) clearTimeout(timer);
});
}
function linkAbort(parent, child) {
if (!parent || !child) return;
if (parent.aborted) {
try {
child.abort();
} catch (_) {}
return;
}
parent.addEventListener(
'abort',
() => {
try {
child.abort();
} catch (_) {}
},
{ once: true },
);
}
function fetchWithTimeout(url, opts, timeoutMs) {
const ms = Number(timeoutMs) > 0 ? Number(timeoutMs) : WEB_TIMEOUT_MS;
if (!(ms > 0)) return Promise.reject(abortError(0));
const headers = Object.assign({ 'user-agent': BROWSER_UA }, (opts && opts.headers) || {});
const controller = typeof AbortController === 'function' ? new AbortController() : null;
let timer;
const init = Object.assign({}, opts || {}, { headers });
if (controller) init.signal = controller.signal;
if (controller) {
init.signal = controller.signal;
if (opts && opts.signal) linkAbort(opts.signal, controller);
}
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => {
try {
@@ -84,7 +137,8 @@ function fetchWithTimeout(url, opts, timeoutMs) {
}
function readBodyWithTimeout(res, timeoutMs) {
const ms = Number(timeoutMs) > 0 ? Number(timeoutMs) : WEB_TIMEOUT_MS;
const ms = Number(timeoutMs) > 0 ? Number(timeoutMs) : 0;
if (!(ms > 0)) return Promise.reject(abortError(0));
if (!res || typeof res.text !== 'function') return Promise.resolve('');
let timer;
const timeout = new Promise((_, reject) => {
@@ -238,14 +292,16 @@ function pushHit(hits, seen, href, title, snippet, limit) {
}
async function fetchText(url, timeoutMs, opts) {
const ms = Number(timeoutMs) > 0 ? Number(timeoutMs) : WEB_TIMEOUT_MS;
const deadline = Date.now() + ms;
try {
net.assertPublicHttpUrl(url);
} catch (err) {
return { error: String(err && err.message || err), url };
}
try {
const res = await fetchWithTimeout(url, opts || {}, timeoutMs);
const text = await readBodyWithTimeout(res, timeoutMs);
const res = await fetchWithTimeout(url, opts || {}, remainingMs(deadline));
const text = await readBodyWithTimeout(res, remainingMs(deadline));
if (res.status >= 400) {
return { error: 'HTTP ' + res.status, url: String(res.url || url), status: res.status, text };
}
@@ -381,9 +437,11 @@ function isDdgChallenge(html) {
}
async function duckDuckGoSearch(query, timeoutMs, limit) {
const ms = Number(timeoutMs) > 0 ? Number(timeoutMs) : WEB_TIMEOUT_MS;
const deadline = Date.now() + ms;
const url = 'https://html.duckduckgo.com/html/';
const body = 'q=' + encodeURIComponent(query) + '&b=&kl=us-en';
let page = await fetchText(url, timeoutMs, {
let page = await fetchText(url, remainingMs(deadline), {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
@@ -397,7 +455,8 @@ async function duckDuckGoSearch(query, timeoutMs, limit) {
const posted = parseDdgHtmlHits(page.text, limit);
if (searchHasHits(posted)) return posted;
}
page = await fetchText(url + '?q=' + encodeURIComponent(query), timeoutMs);
if (remainingMs(deadline) <= 0) return { error: 'timed out after ' + ms + 'ms', url };
page = await fetchText(url + '?q=' + encodeURIComponent(query), remainingMs(deadline));
if (page.error) return page;
if (isDdgChallenge(page.text)) return { error: 'duckduckgo bot challenge', url: page.url, status: page.status };
return parseDdgHtmlHits(page.text, limit);
@@ -633,32 +692,48 @@ async function runWebSearch(query, opts) {
const q = String(query || '').trim();
if (!q) return { error: 'query required' };
const limit = clampLimit(opts.limit);
const timeoutMs = opts.timeoutMs;
const budget = budgetMs(opts.timeoutMs, SEARCH_BUDGET_MS, SEARCH_BUDGET_MS);
const deadline = Date.now() + budget;
const engine = resolveEngine(opts.engine);
if (engine !== 'auto') {
const fn = SEARCH_ENGINES[engine];
if (!fn) return { error: 'unknown engine', engine: opts.engine, engines: ENGINE_NAMES };
const result = await fn(q, timeoutMs, limit);
if (searchHasHits(result)) return tagSearchHits(result, engine).slice(0, limit);
return {
error: (result && result.error) || 'no search results',
url: result && result.url,
tried: [engine],
engines: ENGINE_NAMES,
};
}
const prefer = Array.isArray(opts.prefer) ? opts.prefer.map(resolveEngine).filter((n) => SEARCH_ENGINES[n]) : [];
const chain = prefer.concat(AUTO_ENGINES.filter((name) => prefer.indexOf(name) < 0));
const tried = [];
const errors = {};
for (let i = 0; i < chain.length; i++) {
const name = chain[i];
tried.push(name);
const result = await SEARCH_ENGINES[name](q, timeoutMs, limit);
if (searchHasHits(result)) return tagSearchHits(result, name).slice(0, limit);
errors[name] = result && result.error ? result.error : 'no results';
}
return { error: 'no search results', tried, errors, engines: ENGINE_NAMES };
const timedOut = () => timeoutErrorResult(budget, {
tried: tried.slice(),
errors: Object.assign({}, errors),
engines: ENGINE_NAMES,
});
return withDeadline(async () => {
try {
if (engine !== 'auto') {
const fn = SEARCH_ENGINES[engine];
if (!fn) return { error: 'unknown engine', engine: opts.engine, engines: ENGINE_NAMES };
tried.push(engine);
const result = await fn(q, remainingMs(deadline), limit);
if (searchHasHits(result)) return tagSearchHits(result, engine).slice(0, limit);
return {
error: (result && result.error) || 'no search results',
url: result && result.url,
tried: [engine],
engines: ENGINE_NAMES,
};
}
const prefer = Array.isArray(opts.prefer) ? opts.prefer.map(resolveEngine).filter((n) => SEARCH_ENGINES[n]) : [];
const chain = prefer.concat(AUTO_ENGINES.filter((name) => prefer.indexOf(name) < 0));
for (let i = 0; i < chain.length; i++) {
const left = remainingMs(deadline);
if (left <= 0) return timedOut();
if (tried.length && left < 50) break;
const name = chain[i];
tried.push(name);
const result = await SEARCH_ENGINES[name](q, Math.min(ENGINE_TIMEOUT_MS, left), limit);
if (searchHasHits(result)) return tagSearchHits(result, name).slice(0, limit);
errors[name] = result && result.error ? result.error : 'no results';
}
return { error: 'no search results', tried, errors, engines: ENGINE_NAMES };
} catch (err) {
return { error: String(err && err.message || err), tried, errors, engines: ENGINE_NAMES };
}
}, deadline, timedOut);
}
async function googleSearchWithFallback(query, timeoutMs) {
@@ -672,22 +747,27 @@ async function webSearch(query, timeoutMs) {
async function codeSearch(query, timeoutMs, limit) {
const q = String(query || '').trim();
if (!q) return { error: 'query required' };
const [github, npm, mdn] = await Promise.all([
githubSearch(q, timeoutMs, limit),
npmSearch(q, timeoutMs, limit),
mdnSearch(q, timeoutMs, limit),
]);
const out = { github: [], npm: [], mdn: [] };
if (searchHasHits(github)) out.github = tagSearchHits(github, 'github');
else if (github && github.error) out.github_error = github.error;
if (searchHasHits(npm)) out.npm = tagSearchHits(npm, 'npm');
else if (npm && npm.error) out.npm_error = npm.error;
if (searchHasHits(mdn)) out.mdn = tagSearchHits(mdn, 'mdn');
else if (mdn && mdn.error) out.mdn_error = mdn.error;
if (!out.github.length && !out.npm.length && !out.mdn.length) {
return { error: 'no code search results', github_error: out.github_error, npm_error: out.npm_error, mdn_error: out.mdn_error };
}
return out;
const budget = budgetMs(timeoutMs, SEARCH_BUDGET_MS, SEARCH_BUDGET_MS);
const deadline = Date.now() + budget;
return withDeadline(async () => {
const slice = remainingMs(deadline);
const [github, npm, mdn] = await Promise.all([
githubSearch(q, slice, limit),
npmSearch(q, slice, limit),
mdnSearch(q, slice, limit),
]);
const out = { github: [], npm: [], mdn: [] };
if (searchHasHits(github)) out.github = tagSearchHits(github, 'github');
else if (github && github.error) out.github_error = github.error;
if (searchHasHits(npm)) out.npm = tagSearchHits(npm, 'npm');
else if (npm && npm.error) out.npm_error = npm.error;
if (searchHasHits(mdn)) out.mdn = tagSearchHits(mdn, 'mdn');
else if (mdn && mdn.error) out.mdn_error = mdn.error;
if (!out.github.length && !out.npm.length && !out.mdn.length) {
return { error: 'no code search results', github_error: out.github_error, npm_error: out.npm_error, mdn_error: out.mdn_error };
}
return out;
}, deadline, () => timeoutErrorResult(budget));
}
async function webFetch(url, timeoutMs) {
@@ -696,9 +776,11 @@ async function webFetch(url, timeoutMs) {
} catch (err) {
return { error: String(err && err.message || err), url: String(url || '') };
}
const ms = budgetMs(timeoutMs, WEB_TIMEOUT_MS, SEARCH_BUDGET_MS);
const deadline = Date.now() + ms;
try {
const res = await fetchWithTimeout(url, {}, timeoutMs);
let text = htmlToText(await readBodyWithTimeout(res, timeoutMs));
const res = await fetchWithTimeout(url, {}, remainingMs(deadline));
let text = htmlToText(await readBodyWithTimeout(res, remainingMs(deadline)));
text = truncate.truncateWithMarker(text, 12000);
const href = String(res.url || url);
if (res.status >= 400) {
@@ -718,26 +800,37 @@ async function fetchPage(url, timeoutMs) {
}
const target = String(url);
const jinaUrl = 'https://r.jina.ai/' + target;
const ms = Number(timeoutMs) > 0 ? Number(timeoutMs) : PAGE_TIMEOUT_MS;
try {
net.assertPublicHttpUrl(jinaUrl);
const res = await fetchWithTimeout(jinaUrl, { headers: { accept: 'text/plain', 'user-agent': AGENT_UA } }, ms);
if (res.status < 400) {
let text = await readBodyWithTimeout(res, ms);
text = truncate.truncateWithMarker(text, 12000);
if (text && text.length > 24 && !/verifying you are (a )?human/i.test(text)) {
return { status: res.status, url: target, text, via: 'jina' };
const ms = budgetMs(timeoutMs, PAGE_TIMEOUT_MS, SEARCH_BUDGET_MS);
const deadline = Date.now() + ms;
const timedOut = () => timeoutErrorResult(ms, { url: target });
return withDeadline(async () => {
try {
net.assertPublicHttpUrl(jinaUrl);
const left = remainingMs(deadline);
if (left > 0) {
const res = await fetchWithTimeout(jinaUrl, { headers: { accept: 'text/plain', 'user-agent': AGENT_UA } }, left);
if (res.status < 400) {
let text = await readBodyWithTimeout(res, remainingMs(deadline));
text = truncate.truncateWithMarker(text, 12000);
if (text && text.length > 24 && !/verifying you are (a )?human/i.test(text)) {
return { status: res.status, url: target, text, via: 'jina' };
}
}
}
}
} catch (_) {}
const raw = await webFetch(target, timeoutMs);
if (raw && !raw.via) raw.via = 'raw';
return raw;
} catch (_) {}
const left = remainingMs(deadline);
if (left <= 0) return timedOut();
const raw = await webFetch(target, left);
if (raw && !raw.via) raw.via = 'raw';
return raw;
}, deadline, timedOut);
}
module.exports = {
WEB_TIMEOUT_MS,
PAGE_TIMEOUT_MS,
SEARCH_BUDGET_MS,
ENGINE_TIMEOUT_MS,
BROWSER_UA,
GOOGLE_UA,
AGENT_UA,