wave 2 (platform stream): settings storage & plumbing foundation (P2)

- src/core/settings.ts: versioned DELTA record schema + sanitizer (unknown keys preserved)
  — general/chat/agent{models,tools,prompts}/appearance/graph sections
- src/core/settings-resolution.ts: pure resolveSettings (factory defaults → user delta),
  extraction/graph-context channel predicates
- src/store/settings.ts + prompt-overrides.ts: SettingsStore/PromptOverrideStore adapters
  wired through stores.ts (effectiveSettings() fresh per call); lazy migration of
  default-settings.json into record.chat
- app/api/settings/** routes (record, section bodies, prompt overrides per segment)
- chat-handler: graph gates at trigger call-sites + toolToggles injection seam
- config.ts: SETTINGS_FILE + PROMPTS_OVERRIDE_DIR paths

587/587 tests; typecheck/lint/build clean
This commit is contained in:
amosblomqvist
2026-09-19 18:05:30 +03:00
parent 36c5d19437
commit cb4fe25c89
24 changed files with 2096 additions and 101 deletions
+42
View File
@@ -30,6 +30,7 @@ import type {
ClusterLabeling,
EmbeddingsLedger,
} from "./contracts.ts";
import type { UserSettings } from "./settings.ts";
// ─── ChatStore — the session record's home (data/chats) ─────────────────────
@@ -148,3 +149,44 @@ export interface JobStore {
/** The job fulfilling a given tool part, if any (spawn-hook dedup). */
findJobByToolCallId(chatId: string, toolCallId: string): JobRecord | null;
}
// ─── SettingsStore — THE user settings record (settings-storage wave, 2026-09-19) ──
/**
* The one record for every non-secret product preference (core/settings.ts
* UserSettings — delta semantics, see that module's doc). The store is
* replace-whole on the RECORD; per-section scoping is a server-band concern
* (single process, no stale-client hazard to defend against at this seam).
* Lazy migration (default-settings.json → the record's chat section, schema
* version bumps) happens inside readSettings, never in the callers.
*/
export interface SettingsStore {
/** The sanitized record, or null when none exists yet (first boot). */
readSettings(): UserSettings | null;
/** Persist the record (replace-whole). Callers pass a SANITIZED record. */
writeSettings(settings: UserSettings): void;
}
// ─── PromptOverrideStore — per-segment system-prompt overrides (2026-09-19) ──
/**
* User text overriding ONE source prompt segment (the shared base or one
* numeric fragment), stored at the data-dir seam mirroring the source
* layout (config.PROMPTS_OVERRIDE_DIR). File presence is NOT the override:
* the settings record's agent.prompts switch is the explicit on/off, so a
* user can keep a customized file on disk while running the source default.
* The segment LIST is always derived from source at runtime — see the
* adapter (src/store/prompt-overrides.ts) for the loader rules.
*/
export interface PromptOverrideStore {
/** Every override path currently on disk ("system-prompt.md",
* "teacher/20-teaching.md", ...), POSIX-relative, sorted. */
listOverrides(): string[];
/** The override text, or null when the path has no file. */
readOverride(segmentPath: string): string | null;
/** Persist override text for a segment path (creates parents). */
writeOverride(segmentPath: string, text: string): void;
/** Remove the override file, if present (switches are untouched — an
* on-switch with no file composes the source default, inert). */
deleteOverride(segmentPath: string): void;
}