feat: distill any oversized goal objective, not just plans

The distillation moved from PlanView into setSessionGoal — the shared
choke point for every goal source (composer, fork dialog, plans) — and
into the scheduled-tasks goal creation server-side. Any objective over
5000 chars is distilled into completion criteria by the small model; on a
transient failure a head+tail excerpt keeps the intent (top) and the
acceptance criteria (bottom) with a trim marker, plus a toast. The
working agent always received the full prompt in chat — only the auditor
is bound by the limit.
This commit is contained in:
Bohdan Triapitsyn
2026-07-12 16:34:16 +03:00
parent 19b2a3d0d3
commit 8d956f5f9c
14 changed files with 99 additions and 51 deletions
@@ -447,7 +447,41 @@ export const createScheduledTasksRuntime = (deps) => {
// File-backed objective keyed by session id: metadata stays light, the
// full expanded prompt lives under the OpenChamber data dir. If the file
// write fails, fall back to an inline (clamped) objective.
const objectiveText = expandSnippets(task.execution.prompt, projectPath);
// Oversized prompts are distilled into audit criteria by the small model
// (the working agent gets the full prompt in chat anyway); on distill
// failure a head+tail excerpt keeps intent and acceptance criteria.
let objectiveText = expandSnippets(task.execution.prompt, projectPath);
if (objectiveText.length > 5000) {
let distilled = null;
try {
const { generateSmallModelText } = await import('../small-model/index.js');
const generated = await generateSmallModelText({
restrictToPreferredProvider: true,
prompt: objectiveText,
system: [
'You distill a large task description into the COMPLETION CRITERIA a progress auditor will judge against.',
'Return ONLY the criteria text — no preamble, no headers, no markdown fences.',
'Capture: the end goals, what must exist and work when the task is fully done, and how each major part is verified. Omit implementation steps.',
'Preserve verbatim any file paths, commands, and identifiers that define the task.',
'Stay under 4000 characters.',
'Write in the same language as the task text.',
].join('\n'),
directory: projectPath,
preferredProviderID: task.execution.providerID,
preferredModelID: task.execution.modelID,
});
distilled = typeof generated?.text === 'string' ? generated.text.trim() : null;
} catch (error) {
console.warn('[scheduled-tasks] goal objective distillation failed:', error?.message || error);
}
if (distilled) {
objectiveText = distilled;
} else {
const marker = '\n\n[… objective trimmed for the auditor — the full prompt was delivered in the chat message …]\n\n';
const half = Math.max(0, Math.floor((5000 - marker.length) / 2));
objectiveText = `${objectiveText.slice(0, half)}${marker}${objectiveText.slice(-half)}`;
}
}
let objectiveFile = false;
try {
const { writeObjective } = await import('../session-goal/objectives.js');