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
+29
View File
@@ -1,5 +1,7 @@
import React from 'react';
import { useSession } from '@/sync/sync-context';
import { getSessionGoal, type SessionGoalPayload } from '@/lib/sessionGoalMetadata';
import { fetchGoalObjectiveContent } from '@/lib/sessionGoalActions';
import { useUIStore } from '@/stores/useUIStore';
export interface SessionGoalState {
@@ -19,3 +21,30 @@ export function useSessionGoal(sessionId: string, directory?: string): SessionGo
enabled,
};
}
// Effective objective text for display. Inline goals return the metadata
// text directly; file-backed goals fetch the server-side file once per
// goal edit (keyed by id + updatedAt). Display-only: a failed fetch yields
// null and callers degrade gracefully (e.g. VS Code, where the OpenChamber
// route is unavailable — the strip then shows only the audit note).
export function useGoalObjectiveContent(sessionId: string, goal: SessionGoalPayload | null): string | null {
const [fetched, setFetched] = React.useState<string | null>(null);
const fetchKey = goal?.objectiveFile ? `${sessionId}:${goal.id}:${goal.updatedAt}` : '';
React.useEffect(() => {
if (!fetchKey) {
setFetched(null);
return undefined;
}
let alive = true;
void fetchGoalObjectiveContent(sessionId).then((content) => {
if (alive) setFetched(content);
});
return () => {
alive = false;
};
}, [fetchKey, sessionId]);
if (!goal) return null;
return goal.objectiveFile ? fetched : goal.objective;
}