feat: file-backed goal objectives + compaction accounting fixes

Compaction fixes (observed in a real long run):
- the summary message's zeroed tokens froze the goal counter at its
  pre-compaction value; segments now close with the previously displayed
  total as a continuity floor
- audits and continuations after a summary tail now take execution params
  (provider/model/agent/variant) from the newest non-summary assistant
  turn instead of inheriting agent 'compaction' and the summarize model

File-backed objectives:
- the objective text lives in <data-dir>/goals/<sessionId>.md, keyed by
  session id (one goal per session, a new goal overwrites the file);
  metadata carries only an objectiveFile flag so session.updated fanout
  stays light, and never a path — ids are pattern-validated before any
  filesystem access
- limit raised to 5000 chars, no snapshot field: the UI fetches content
  via PUT/GET/DELETE /api/goals/objective/:sessionId (behind the blanket
  /api auth gate), writes the file before stamping metadata, and falls
  back to an inline objective when the write fails
- the loop reads the file fresh on every tick, so objectives are
  live-editable mid-goal; a missing file falls back to the inline text
- scheduled goal tasks write the objective file server-side; VS Code
  degrades to the audit note (route unavailable there by design)
This commit is contained in:
Bohdan Triapitsyn
2026-07-12 02:49:56 +03:00
parent 2c4b40893c
commit c9ac8676e7
12 changed files with 288 additions and 19 deletions
@@ -444,9 +444,22 @@ export const createScheduledTasksRuntime = (deps) => {
// from session events like any other goal.
const createTaskGoal = async ({ baseUrl, authHeaders, sessionID, projectPath, task }) => {
const now = Date.now();
// 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);
let objectiveFile = false;
try {
const { writeObjective } = await import('../session-goal/objectives.js');
await writeObjective(sessionID, objectiveText);
objectiveFile = true;
} catch (error) {
console.warn('[scheduled-tasks] goal objective file write failed, falling back to inline:', error?.message || error);
}
const goal = {
id: `${now.toString(36)}${Math.random().toString(36).slice(2, 8)}`,
objective: expandSnippets(task.execution.prompt, projectPath).slice(0, 2000),
objective: objectiveFile ? '' : objectiveText.slice(0, 5000),
objectiveFile,
status: 'active',
tokenBudget: task.execution.goalTokenBudget || null,
tokensUsed: 0,