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[];
|
||||
|
||||
@@ -106,12 +106,15 @@ describe("collaboration integration flow", () => {
|
||||
|
||||
const state = snapshot.load();
|
||||
const evalRun = state.evalRuns.find((run) => run.id === evalRunId);
|
||||
const snapshotRecord = state.skillRuntimeSnapshots[0];
|
||||
// Compiled runtime snapshots live in the database only - the renderer
|
||||
// snapshot must not carry per-run compiled prompts.
|
||||
const snapshotRecord = connection.db.prepare("select compiled_prompt from skill_runtime_snapshots limit 1")
|
||||
.get() as { compiled_prompt: string } | undefined;
|
||||
if (!evalRun) throw new Error("Integration eval run was not created.");
|
||||
if (!snapshotRecord) throw new Error("Skill runtime snapshot was not created.");
|
||||
expect(state.approvals.find((approval) => approval.id === approvalId)?.state).toBe(APPROVAL_STATE.APPROVED);
|
||||
expect(state.approvalAssignments.some((assignment) => assignment.approvalId === approvalId)).toBe(true);
|
||||
expect(snapshotRecord.compiledPrompt).toContain("Release Gate");
|
||||
expect(snapshotRecord.compiled_prompt).toContain("Release Gate");
|
||||
expect(evalRun).toMatchObject({ state: SDLC_RUN_STATE.PASSED, failedCount: 0 });
|
||||
expect(state.operationalEvents.some((event) => event.signalKey === "skill_resolve_success")).toBe(true);
|
||||
connection.close();
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("domain services", () => {
|
||||
const connection = createConnection();
|
||||
|
||||
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50]);
|
||||
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
@@ -189,15 +189,24 @@ describe("domain services", () => {
|
||||
skills.resolveRuntimeSnapshot({ orgId, agentId, runId: "run-review-1" });
|
||||
});
|
||||
|
||||
// Catalogue metadata ships in the snapshot; skill internals stay in the
|
||||
// database (the snapshot rides IPC on every refresh).
|
||||
const state = snapshot.load();
|
||||
expect(state.skillDefinitions[0]?.slug).toBe("review-routes");
|
||||
expect(state.skillRevisions[0]?.versionSemver).toBe("1.0.0");
|
||||
expect(state.skillRevisions[0]?.immutableChecksum).toHaveLength(64);
|
||||
expect(state.skillRevisions[0]?.manifestJson).toContain("architecture-review");
|
||||
expect(state.skillTests[0]?.lastResult).toContain("\"status\":\"passed\"");
|
||||
expect(state.skillTests[0]?.lastRunAt).toBeTruthy();
|
||||
expect(state.skillRuntimeSnapshots[0]?.sourceRevisionIds).toContain(revisionId);
|
||||
expect(state.skillRuntimeSnapshots[0]?.compiledPrompt).toContain("# Review Routes");
|
||||
expect(state).not.toHaveProperty("skillRuntimeSnapshots");
|
||||
const revision = connection.db.prepare("select version_semver, immutable_checksum, manifest_json from skill_revisions where id = ?")
|
||||
.get(revisionId) as { version_semver: string; immutable_checksum: string; manifest_json: string };
|
||||
expect(revision.version_semver).toBe("1.0.0");
|
||||
expect(revision.immutable_checksum).toHaveLength(64);
|
||||
expect(revision.manifest_json).toContain("architecture-review");
|
||||
const skillTest = connection.db.prepare("select last_result, last_run_at from skill_tests where revision_id = ?")
|
||||
.get(revisionId) as { last_result: string; last_run_at: string };
|
||||
expect(skillTest.last_result).toContain("\"status\":\"passed\"");
|
||||
expect(skillTest.last_run_at).toBeTruthy();
|
||||
const runtime = connection.db.prepare("select source_revision_ids, compiled_prompt from skill_runtime_snapshots where run_id = 'run-review-1'")
|
||||
.get() as { source_revision_ids: string; compiled_prompt: string };
|
||||
expect(runtime.source_revision_ids).toContain(revisionId);
|
||||
expect(runtime.compiled_prompt).toContain("# Review Routes");
|
||||
expect(state.auditEvents.some((event) => event.entityType === "skill_revision" && event.action === "tests_run")).toBe(true);
|
||||
connection.close();
|
||||
});
|
||||
@@ -238,9 +247,12 @@ describe("domain services", () => {
|
||||
.get(orgA, "review-routes") as { id: string }).id;
|
||||
expect(() => skills.bind({ orgId: orgA, agentId: agentB, skillId, revisionId })).toThrow("Agent was not found.");
|
||||
skills.bind({ orgId: orgA, agentId: agentA, skillId, revisionId });
|
||||
skills.resolveRuntimeSnapshot({ orgId: orgA, agentId: agentA, runId: "run-review-1" });
|
||||
expect(() => skills.resolveRuntimeSnapshot({ orgId: orgA, agentId: agentA, runId: "run-review-1" }))
|
||||
.toThrow("Skill runtime snapshot already exists for this run.");
|
||||
const firstResolve = skills.resolveRuntimeSnapshot({ orgId: orgA, agentId: agentA, runId: "run-review-1" });
|
||||
// Idempotent per run: a recovered run reuses its frozen snapshot
|
||||
// instead of failing on the uniqueness guard.
|
||||
expect(skills.resolveRuntimeSnapshot({ orgId: orgA, agentId: agentA, runId: "run-review-1" })).toBe(firstResolve);
|
||||
expect(connection.db.prepare("select count(*) as n from skill_runtime_snapshots where run_id = 'run-review-1'").get())
|
||||
.toEqual({ n: 1 });
|
||||
});
|
||||
|
||||
expect(agentA).not.toBe(agentB);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { DatabaseConnection } from "../../src/main/database/connection";
|
||||
import { OrganizationService } from "../../src/main/services/organization-service";
|
||||
import { SnapshotService } from "../../src/main/services/snapshot-service";
|
||||
|
||||
/**
|
||||
* The snapshot is serialized over IPC on every refresh, so its size is a
|
||||
* user-facing performance contract. A real profile once accumulated 144MB of
|
||||
* per-run compiled skill prompts that rode along on every load and froze the
|
||||
* whole app - these tests pin that regression class shut.
|
||||
*/
|
||||
describe("snapshot payload", () => {
|
||||
test("per-run compiled prompts and raw provider bodies never ride the snapshot", () => {
|
||||
const connection = new DatabaseConnection(join(tmpdir(), `ac-snapshot-payload-${randomUUID()}.db`));
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const snapshot = new SnapshotService(connection.db);
|
||||
let orgId = "";
|
||||
connection.transaction(() => {
|
||||
orgId = organization.createOrganization({ organizationName: "OrbitOps", employeeName: "Yang", mission: "m", goal: "g" });
|
||||
const agentId = organization.createAgent({
|
||||
orgId, name: "Ada", role: "Engineer", department: "Engineering",
|
||||
modelProviderId: null, modelId: null, budgetCents: 1000, managerAgentId: null,
|
||||
});
|
||||
const providerId = (connection.db.prepare("select id from providers where org_id = ? limit 1").get(orgId) as { id: string }).id;
|
||||
const megabyte = "x".repeat(1_048_576);
|
||||
connection.db.prepare(`
|
||||
insert into skill_runtime_snapshots(id, agent_id, run_id, compiled_prompt, tools_manifest, source_revision_ids, created_at)
|
||||
values ('srs_fat', ?, 'run_fat', ?, '{}', '[]', ?)
|
||||
`).run(agentId, megabyte, new Date().toISOString());
|
||||
connection.db.prepare(`
|
||||
insert into provider_requests(id, org_id, provider_id, provider_key, model, prompt_sha256, response_text, raw_response_json, state, error_message, created_at, completed_at)
|
||||
values ('prq_fat', ?, ?, 'openai', 'gpt-5.4', 'sha', 'short answer', ?, 'succeeded', null, ?, ?)
|
||||
`).run(orgId, providerId, megabyte, new Date().toISOString(), new Date().toISOString());
|
||||
});
|
||||
|
||||
const state = snapshot.load();
|
||||
expect(state).not.toHaveProperty("skillRuntimeSnapshots");
|
||||
expect(state).not.toHaveProperty("skillRevisions");
|
||||
const request = state.providerRequests.find((row) => row.id === "prq_fat");
|
||||
expect(request?.responseText).toBe("short answer");
|
||||
expect(request).not.toHaveProperty("rawResponseJson");
|
||||
// Two megabytes of database bloat must not translate into snapshot bytes.
|
||||
expect(JSON.stringify(state).length).toBeLessThan(400_000);
|
||||
connection.close();
|
||||
});
|
||||
});
|
||||
@@ -114,17 +114,19 @@ describe("task execution service", () => {
|
||||
const state = snapshot.load();
|
||||
const run = state.taskRuns.find((item) => item.id === runId);
|
||||
const request = state.providerRequests.find((item) => item.id === run?.providerRequestId);
|
||||
const skillRuntimeSnapshot = state.skillRuntimeSnapshots.find((item) => item.runId === runId);
|
||||
const skillRuntimeSnapshot = connection.db.prepare(
|
||||
"select agent_id, run_id, compiled_prompt, source_revision_ids from skill_runtime_snapshots where run_id = ?",
|
||||
).get(runId) as { agent_id: string; run_id: string; compiled_prompt: string; source_revision_ids: string } | undefined;
|
||||
|
||||
expect(run).toMatchObject({ taskId, agentId: expect.any(String), providerId, state: "completed", errorMessage: null });
|
||||
expect(request).toMatchObject({ providerId, workspaceId, state: "succeeded", responseText: "executed:gpt-5.4" });
|
||||
expect(skillRuntimeSnapshot).toMatchObject({ agentId, runId });
|
||||
expect(skillRuntimeSnapshot).toMatchObject({ agent_id: agentId, run_id: runId });
|
||||
expect(run?.executionContextId).toMatch(/^ctx_/);
|
||||
expect(run?.serviceIdentityId).toMatch(/^sid_/);
|
||||
expect(JSON.parse(run?.workspaceMountJson ?? "[]")).toEqual([expect.objectContaining({ workspaceId, mode: "read_only" })]);
|
||||
expect(JSON.parse(run?.secretScopeJson ?? "{}")).toMatchObject({ providerIds: [providerId], credentialAccess: "provider_runtime_only" });
|
||||
expect(skillRuntimeSnapshot?.compiledPrompt).toContain("Verify release readiness");
|
||||
expect(skillRuntimeSnapshot?.sourceRevisionIds).toContain(revisionId);
|
||||
expect(skillRuntimeSnapshot?.compiled_prompt).toContain("Verify release readiness");
|
||||
expect(skillRuntimeSnapshot?.source_revision_ids).toContain(revisionId);
|
||||
expect(capturedPrompt).toContain("Task: Draft launch checklist");
|
||||
expect(capturedPrompt).toContain("Assignee: Ada (Execution lead)");
|
||||
expect(capturedPrompt).toContain("Workspace: Task Workspace");
|
||||
|
||||
Reference in new issue
Block a user