Stop shipping megabytes of skill prompts in every snapshot refresh
The whole app turned to molasses because snapshot.load() shipped the entire skill_runtime_snapshots table - unbounded, compiled_prompt included - over IPC on every refresh. A real profile carried 142 rows totaling 144MB (1.1MB each from the pre-cap CEO compile-all era), so every coalesced refresh serialized ~144MB of JSON. The forced-humanizer rework made it worse by adding a row per run for every agent. - Drop skillRuntimeSnapshots, skillRevisions, skillTests, and skillBindings from the renderer snapshot: no renderer surface reads them; skill internals are main-process state. Only the lightweight skillDefinitions catalogue still ships. - Project provider_requests without raw_response_json (4.1MB across the newest 100 rows; RunsPage only reads responseText) - Migration 051 purges the bug-era compiled prompts from existing DBs - resolveRuntimeSnapshot is now idempotent per run: a run re-queued by startup recovery reuses its frozen snapshot instead of dying on the uniqueness guard and burning real retry budget - Regression test pins the contract: a 2MB database row must not add bytes to the snapshot (payload asserted under 400KB)
This commit is contained in:
1 parent
4ab20e679c
commit
1f89ea7089
10 files changed
+117
-34
No files matched your search
@@ -0,0 +1,10 @@
|
||||
// Rows written before the compiled-prompt cap carried the entire skill
|
||||
// catalogue (~1.1MB each; 144MB observed in a real profile). Nothing reads a
|
||||
// finished run's compiled prompt back, and an interrupted run simply
|
||||
// recompiles under the cap on recovery, so the table drains cleanly.
|
||||
export const purgeRuntimeSnapshotBloatSchema = `
|
||||
delete from skill_runtime_snapshots;
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (51, 'purge_runtime_snapshot_bloat', datetime('now'));
|
||||
`;
|
||||
@@ -49,6 +49,7 @@ import { authConfigPurgeSchema } from "./migrations/047-auth-config-purge";
|
||||
import { uiPreferencesSchema } from "./migrations/048-ui-preferences";
|
||||
import { avatarsSchema } from "./migrations/049-avatars";
|
||||
import { financeSalesDepartmentsSchema } from "./migrations/050-finance-sales-departments";
|
||||
import { purgeRuntimeSnapshotBloatSchema } from "./migrations/051-purge-runtime-snapshot-bloat";
|
||||
|
||||
interface Migration {
|
||||
version: number;
|
||||
@@ -107,6 +108,7 @@ const MIGRATIONS: Migration[] = [
|
||||
{ version: 48, name: "ui_preferences", sql: uiPreferencesSchema },
|
||||
{ version: 49, name: "avatars", sql: avatarsSchema },
|
||||
{ version: 50, name: "finance_sales_departments", sql: financeSalesDepartmentsSchema },
|
||||
{ version: 51, name: "purge_runtime_snapshot_bloat", sql: purgeRuntimeSnapshotBloatSchema },
|
||||
];
|
||||
|
||||
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
||||
|
||||
@@ -178,9 +178,12 @@ export class SkillService {
|
||||
* compiled prompt instead of silently overflowing the model's context.
|
||||
*/
|
||||
resolveRuntimeSnapshot(input: ResolveAgentSkillsInput): string {
|
||||
// Idempotent per run: a run interrupted after resolving (crash, quit) is
|
||||
// re-queued by startup recovery and must reuse its frozen skill set, not
|
||||
// die on a uniqueness error that burns real retry budget.
|
||||
const existingSnapshot = this.db.prepare("select id from skill_runtime_snapshots where agent_id = ? and run_id = ?")
|
||||
.get(input.agentId, input.runId) as { id: string } | undefined;
|
||||
if (existingSnapshot) throw new Error("Skill runtime snapshot already exists for this run.");
|
||||
if (existingSnapshot) return existingSnapshot.id;
|
||||
const forcedRows = this.db.prepare(`
|
||||
select
|
||||
sd.id as skill_id,
|
||||
|
||||
@@ -62,8 +62,6 @@ export class SnapshotService {
|
||||
);
|
||||
const providerUsageWindows = list<ProviderUsageWindowRow>(this.db, "provider_usage_windows", "window_start desc");
|
||||
const providerRateLimits = buildProviderRateLimits(providers, providerUsageWindows);
|
||||
const skillBindings = list<AppSnapshot["skillBindings"][number]>(this.db, "skill_bindings", "priority asc")
|
||||
.map((binding) => ({ ...binding, enabled: toBool(binding.enabled) }));
|
||||
const files = list<AppSnapshot["files"][number]>(this.db, "file_objects", "created_at desc")
|
||||
.map((file) => ({ ...file, pinned: toBool(file.pinned) }));
|
||||
const legalHolds = list<AppSnapshot["legalHolds"][number]>(this.db, "legal_holds", "created_at desc")
|
||||
@@ -115,14 +113,18 @@ export class SnapshotService {
|
||||
providerAuthProfiles,
|
||||
providerAuthSessions,
|
||||
providerCatalog: PROVIDER_CATALOG,
|
||||
providerRequests: recentList(this.db, "provider_requests", "created_at desc", 100),
|
||||
// Explicit projection: raw_response_json alone weighs megabytes across
|
||||
// 100 rows and no renderer surface reads it.
|
||||
providerRequests: all<Row>(this.db.prepare(`
|
||||
select id, org_id, provider_id, provider_key, workspace_id, model, prompt_sha256,
|
||||
response_text, state, error_message, input_char_count, output_char_count,
|
||||
execution_mode, policy_decision, external_response_id, stream_event_count,
|
||||
cancelled_at, created_at, completed_at
|
||||
from provider_requests order by created_at desc limit 100
|
||||
`)).map((row) => camel(row) as unknown as AppSnapshot["providerRequests"][number]),
|
||||
providerRateLimits,
|
||||
providerRateLimitObservations,
|
||||
skillDefinitions: list(this.db, "skill_definitions", "created_at asc"),
|
||||
skillRevisions: list(this.db, "skill_revisions", "published_at desc"),
|
||||
skillBindings,
|
||||
skillRuntimeSnapshots: list(this.db, "skill_runtime_snapshots", "created_at desc"),
|
||||
skillTests: list(this.db, "skill_tests", "last_run_at desc, id asc"),
|
||||
socialAccounts: list(this.db, "social_accounts", "created_at asc"),
|
||||
socialAccountGrants: list(this.db, "social_account_grants", "created_at asc"),
|
||||
browserSessionContainers: list<AppSnapshot["browserSessionContainers"][number]>(this.db, "browser_session_containers", "created_at asc")
|
||||
|
||||
@@ -10,6 +10,11 @@ export const PROVIDER_REQUEST_STATE = {
|
||||
|
||||
export type ProviderRequestState = typeof PROVIDER_REQUEST_STATE[keyof typeof PROVIDER_REQUEST_STATE];
|
||||
|
||||
/**
|
||||
* Snapshot projection of a provider request. The raw provider response body
|
||||
* stays in the database (task_runs diagnostics read it there) — shipping it
|
||||
* to the renderer ballooned every snapshot by megabytes for nothing.
|
||||
*/
|
||||
export interface ProviderRequestRecord {
|
||||
id: string;
|
||||
orgId: string;
|
||||
@@ -19,7 +24,6 @@ export interface ProviderRequestRecord {
|
||||
model: string;
|
||||
promptSha256: string;
|
||||
responseText: string | null;
|
||||
rawResponseJson: string | null;
|
||||
state: ProviderRequestState;
|
||||
errorMessage: string | null;
|
||||
inputCharCount: number;
|
||||
|
||||
@@ -34,11 +34,7 @@ import type {
|
||||
ProviderCatalogEntry,
|
||||
ProviderRecord,
|
||||
ScheduledPostRecord,
|
||||
SkillBindingRecord,
|
||||
SkillDefinitionRecord,
|
||||
SkillRevisionRecord,
|
||||
SkillRuntimeSnapshotRecord,
|
||||
SkillTestRecord,
|
||||
SprintRecord,
|
||||
TaskRecord,
|
||||
TaskRunRecord,
|
||||
@@ -103,11 +99,11 @@ export interface AppSnapshot {
|
||||
providerRequests: ProviderRequestRecord[];
|
||||
providerRateLimits: ProviderRateLimitStateRecord[];
|
||||
providerRateLimitObservations: ProviderRateLimitObservationRecord[];
|
||||
// The snapshot is the renderer's working set and rides IPC on every
|
||||
// refresh. Skill internals (revisions, bindings, per-run runtime
|
||||
// snapshots with compiled prompts) are main-process concerns and stay out
|
||||
// of it; only the lightweight catalogue listing ships.
|
||||
skillDefinitions: SkillDefinitionRecord[];
|
||||
skillRevisions: SkillRevisionRecord[];
|
||||
skillBindings: SkillBindingRecord[];
|
||||
skillRuntimeSnapshots: SkillRuntimeSnapshotRecord[];
|
||||
skillTests: SkillTestRecord[];
|
||||
socialAccounts: SocialAccountRecord[];
|
||||
socialAccountGrants: SocialAccountGrantRecord[];
|
||||
browserSessionContainers: BrowserSessionContainerRecord[];
|
||||
|
||||
Reference in new issue
Block a user