compaction: the surface — settings row, two ruled dividers, summary dialog, send gate (round 14 slice 3)

- settings: compaction.thresholdTokens row on the Agent tab (owner pick)
  in a Context group — CommitInput mirroring the graph budget row, hint
  resolved through the SAME core resolver the trigger reads; PUT rejects
  below the 32k floor with the anti-churn rationale; own section
  (compaction) in SETTINGS_SECTIONS + the settings-signal union
- view fold: one marker → two dividers — the minimal MOMENT divider at
  the marker (retrying-message register, no rule lines) and the longer
  CUT divider emitted immediately before the first kept message
  (phase-divider geometry, chip carries '· Nk tok'); both carry the full
  summary payload; full history stays visible to the user
- components/compaction-divider.tsx: the dividers + summary dialog
  (pure exported body for SSR pins, the delete-dialog pattern; Esc +
  Close dismiss, backdrop does not)
- chat.tsx: compaction-changed SSE flips the running state — composer
  canSend, onSend/doSend/doSteerOrSend, and both docks' submit + hint
  ('compacting session…') gate on it (the server's registry mutex is the
  structural gate; this is the affordance); dialog mounts beside the
  abort dialog
- pins: settings-body (4), view fold (2), divider SSR (4)
This commit is contained in:
amosblomqvist
2026-10-05 22:51:08 +03:00
parent dd159cc8fb
commit ffc4df1938
14 changed files with 465 additions and 19 deletions
+31 -1
View File
@@ -17,12 +17,13 @@ import type {
AgentSettings,
AppearanceSettings,
BackgroundModelRole,
CompactionSettings,
GeneralSettings,
GraphSettings,
ModelsSettings,
RoleModelOverride,
} from "../core/settings.ts";
import { BACKGROUND_MODEL_ROLES, EXTRACTION_CHUNK_TOKENS_FLOOR, MODEL_SLOTS } from "../core/settings.ts";
import { BACKGROUND_MODEL_ROLES, COMPACTION_THRESHOLD_FLOOR, EXTRACTION_CHUNK_TOKENS_FLOOR, MODEL_SLOTS } from "../core/settings.ts";
import { WRITABLE_TOOL_KEYS } from "../core/pending.ts";
/** Parsed-or-rejected — `ok: false` carries the 400's error message. */
@@ -342,6 +343,34 @@ export function parseModelsSettingsBody(body: unknown): SettingsBodyResult<Model
return { ok: true, settings };
}
/** compaction (owner round 14): the context-compaction threshold — the
* projected-token budget over which a session summarizes its own history
* into the compaction marker. Strict write side: a present
* thresholdTokens must be a number at or above the floor (the floor is
* anti-churn: each compaction keeps a ~20k-token recent tail, so a
* smaller threshold would re-summarize the tail it just wrote). Absent =
* factory default 150k at resolution. Replace-whole, like every
* section. */
export function parseCompactionSettingsBody(body: unknown): SettingsBodyResult<CompactionSettings> {
const raw = asRecord(body);
if (!raw) return { ok: false, error: "Invalid JSON body" };
const settings: CompactionSettings = {};
if (raw.thresholdTokens !== undefined) {
if (typeof raw.thresholdTokens !== "number" || !Number.isFinite(raw.thresholdTokens)) {
return { ok: false, error: "thresholdTokens must be a number" };
}
const floored = Math.floor(raw.thresholdTokens);
if (floored < COMPACTION_THRESHOLD_FLOOR) {
return {
ok: false,
error: `thresholdTokens must be at least ${COMPACTION_THRESHOLD_FLOOR} (compaction keeps a ~20k-token recent tail — smaller budgets would churn it)`,
};
}
settings.thresholdTokens = floored;
}
return { ok: true, settings };
}
/** The section→parser map for PUT /api/settings/[section] (the route also
* carries the special prompts sub-resource). */
export const SETTINGS_SECTIONS = {
@@ -350,6 +379,7 @@ export const SETTINGS_SECTIONS = {
agent: parseAgentSettingsBody,
appearance: parseAppearanceSettingsBody,
graph: parseGraphSettingsBody,
compaction: parseCompactionSettingsBody,
models: parseModelsSettingsBody,
} as const;