perf(app): reduce startup websocket work
Make provider catalogs compact and content-addressed so repeat launches can reuse the persisted snapshot instead of transferring it again. Remove eager global catalog loading and the stopped-agent defensive tail fetch, while keeping sequence-gap and reconnect recovery as the correctness paths. Add an opt-in production-ID Android profiling build for local startup traces.
This commit is contained in:
@@ -20,6 +20,10 @@ export const CLIENT_CAPS = {
|
||||
providerSubagents: "provider_subagents",
|
||||
// COMPAT(projectUpdates): added in v0.1.109, remove gate after 2027-01-15.
|
||||
projectUpdates: "project_updates",
|
||||
// COMPAT(compactProviderSnapshots): added in v0.2.X. Capable clients receive
|
||||
// provider catalogs with shared thinking sets and may revalidate by content hash.
|
||||
// Remove the legacy snapshot encoding after 2027-02-04.
|
||||
compactProviderSnapshots: "compact_provider_snapshots",
|
||||
browserHost: "browser_host",
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ export const AgentFeatureSchema = z.discriminatedUnion("type", [
|
||||
AgentFeatureSelectSchema,
|
||||
]);
|
||||
|
||||
const AgentModelDefinitionSchema: z.ZodType<AgentModelDefinition> = z.object({
|
||||
const AgentModelDefinitionSchema = z.object({
|
||||
provider: AgentProviderSchema,
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
@@ -266,7 +266,7 @@ const AgentModelDefinitionSchema: z.ZodType<AgentModelDefinition> = z.object({
|
||||
contextWindowMaxTokens: z.number().optional(),
|
||||
thinkingOptions: z.array(AgentSelectOptionSchema).optional(),
|
||||
defaultThinkingOptionId: z.string().optional(),
|
||||
});
|
||||
}) satisfies z.ZodType<AgentModelDefinition>;
|
||||
|
||||
export const ProviderSnapshotEntrySchema = z.object({
|
||||
provider: AgentProviderSchema,
|
||||
@@ -282,6 +282,29 @@ export const ProviderSnapshotEntrySchema = z.object({
|
||||
defaultModeId: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export const CompactProviderSnapshotModelSchema = AgentModelDefinitionSchema.omit({
|
||||
provider: true,
|
||||
thinkingOptions: true,
|
||||
}).extend({
|
||||
thinkingSet: z.number().int().nonnegative().optional(),
|
||||
});
|
||||
|
||||
export const ProviderSnapshotThinkingSetSchema = z.object({
|
||||
options: z.array(AgentSelectOptionSchema),
|
||||
defaultOptionId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const CompactProviderSnapshotEntrySchema = ProviderSnapshotEntrySchema.omit({
|
||||
models: true,
|
||||
}).extend({
|
||||
models: z.array(CompactProviderSnapshotModelSchema).optional(),
|
||||
});
|
||||
|
||||
export const CompactProviderSnapshotSchema = z.object({
|
||||
entries: z.array(CompactProviderSnapshotEntrySchema),
|
||||
thinkingSets: z.array(ProviderSnapshotThinkingSetSchema),
|
||||
});
|
||||
|
||||
const AgentCapabilityFlagsSchema: z.ZodType<AgentCapabilityFlags> = z
|
||||
.object({
|
||||
supportsStreaming: z.boolean(),
|
||||
@@ -1326,6 +1349,8 @@ export const ListAvailableProvidersRequestMessageSchema = z.object({
|
||||
export const GetProvidersSnapshotRequestMessageSchema = z.object({
|
||||
type: z.literal("get_providers_snapshot_request"),
|
||||
cwd: z.string().optional(),
|
||||
// COMPAT(compactProviderSnapshots): old daemons ignore this field and return a full snapshot.
|
||||
ifNoneMatch: z.string().optional(),
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
@@ -4956,6 +4981,9 @@ export const GetProvidersSnapshotResponseMessageSchema = z.object({
|
||||
type: z.literal("get_providers_snapshot_response"),
|
||||
payload: z.object({
|
||||
entries: z.array(ProviderSnapshotEntrySchema),
|
||||
compactSnapshot: CompactProviderSnapshotSchema.optional(),
|
||||
snapshotHash: z.string().optional(),
|
||||
notModified: z.boolean().optional(),
|
||||
generatedAt: z.string(),
|
||||
requestId: z.string(),
|
||||
}),
|
||||
@@ -4967,6 +4995,8 @@ export const ProvidersSnapshotUpdateMessageSchema = z.object({
|
||||
payload: z.object({
|
||||
cwd: z.string().optional(),
|
||||
entries: z.array(ProviderSnapshotEntrySchema),
|
||||
compactSnapshot: CompactProviderSnapshotSchema.optional(),
|
||||
snapshotHash: z.string().optional(),
|
||||
generatedAt: z.string(),
|
||||
}),
|
||||
});
|
||||
@@ -5938,6 +5968,7 @@ export const WSHelloMessageSchema = z.object({
|
||||
[CLIENT_CAPS.terminalReflowableSnapshot]: z.boolean().optional(),
|
||||
[CLIENT_CAPS.providerSubagents]: z.boolean().optional(),
|
||||
[CLIENT_CAPS.projectUpdates]: z.boolean().optional(),
|
||||
[CLIENT_CAPS.compactProviderSnapshots]: z.boolean().optional(),
|
||||
[CLIENT_CAPS.browserHost]: BrowserAutomationHostCapabilitySchema.optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ProviderSnapshotEntry } from "./agent-types.js";
|
||||
import { compactProviderSnapshot, expandProviderSnapshot } from "./provider-snapshot-codec.js";
|
||||
|
||||
const sharedThinking = [
|
||||
{ id: "off", label: "Off", description: "No extra reasoning" },
|
||||
{ id: "minimal", label: "Minimal", description: "Light reasoning" },
|
||||
{ id: "low", label: "Low" },
|
||||
{ id: "medium", label: "Medium", description: "Balanced reasoning" },
|
||||
{ id: "high", label: "High", isDefault: true },
|
||||
{ id: "xhigh", label: "XHigh", description: "Very deep reasoning" },
|
||||
{ id: "max", label: "Max", description: "Extreme reasoning" },
|
||||
];
|
||||
|
||||
function providerEntry(): ProviderSnapshotEntry {
|
||||
return {
|
||||
provider: "pi",
|
||||
status: "ready",
|
||||
enabled: true,
|
||||
models: [
|
||||
{
|
||||
provider: "pi",
|
||||
id: "openai/gpt-a",
|
||||
label: "GPT A",
|
||||
description: "openai/gpt-a",
|
||||
metadata: { provider: "openai", modelId: "gpt-a" },
|
||||
thinkingOptions: sharedThinking,
|
||||
defaultThinkingOptionId: "high",
|
||||
},
|
||||
{
|
||||
provider: "pi",
|
||||
id: "openai/gpt-b",
|
||||
label: "GPT B",
|
||||
metadata: { provider: "openai", modelId: "gpt-b" },
|
||||
thinkingOptions: structuredClone(sharedThinking),
|
||||
defaultThinkingOptionId: "high",
|
||||
},
|
||||
],
|
||||
modes: [{ id: "build", label: "Build" }],
|
||||
};
|
||||
}
|
||||
|
||||
describe("provider snapshot codec", () => {
|
||||
it("shares repeated thinking options while preserving the public snapshot shape", () => {
|
||||
const original = [providerEntry()];
|
||||
const compact = compactProviderSnapshot(original);
|
||||
const expanded = expandProviderSnapshot(compact);
|
||||
|
||||
expect(compact.thinkingSets).toHaveLength(1);
|
||||
expect(compact.entries[0]?.models?.map((model) => model.thinkingSet)).toEqual([0, 0]);
|
||||
expect(expanded).toEqual(original);
|
||||
expect(expanded[0]?.models?.[0]?.thinkingOptions).toBe(
|
||||
expanded[0]?.models?.[1]?.thinkingOptions,
|
||||
);
|
||||
});
|
||||
|
||||
it("shrinks catalogs dominated by one repeated thinking set", () => {
|
||||
const model = providerEntry().models?.[0];
|
||||
if (!model) throw new Error("fixture model missing");
|
||||
const entries: ProviderSnapshotEntry[] = [
|
||||
{
|
||||
provider: "pi",
|
||||
status: "ready",
|
||||
enabled: true,
|
||||
models: Array.from({ length: 200 }, (_, index) => ({
|
||||
...model,
|
||||
id: `openai/model-${index}`,
|
||||
label: `Model ${index}`,
|
||||
metadata: { provider: "openai", modelId: `model-${index}` },
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const legacyBytes = JSON.stringify(entries).length;
|
||||
const compactBytes = JSON.stringify(compactProviderSnapshot(entries)).length;
|
||||
|
||||
expect(compactBytes).toBeLessThan(legacyBytes * 0.5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import type {
|
||||
AgentModelDefinition,
|
||||
AgentSelectOption,
|
||||
ProviderSnapshotEntry,
|
||||
} from "./agent-types.js";
|
||||
|
||||
export interface CompactProviderSnapshotModel extends Omit<
|
||||
AgentModelDefinition,
|
||||
"provider" | "thinkingOptions"
|
||||
> {
|
||||
thinkingSet?: number;
|
||||
}
|
||||
|
||||
export interface CompactProviderSnapshotEntry extends Omit<ProviderSnapshotEntry, "models"> {
|
||||
models?: CompactProviderSnapshotModel[];
|
||||
}
|
||||
|
||||
export interface ProviderSnapshotThinkingSet {
|
||||
options: AgentSelectOption[];
|
||||
defaultOptionId?: string;
|
||||
}
|
||||
|
||||
export interface CompactProviderSnapshot {
|
||||
entries: CompactProviderSnapshotEntry[];
|
||||
thinkingSets: ProviderSnapshotThinkingSet[];
|
||||
}
|
||||
|
||||
function thinkingSetKey(set: ProviderSnapshotThinkingSet): string {
|
||||
return JSON.stringify(set);
|
||||
}
|
||||
|
||||
function compactModel(
|
||||
{
|
||||
provider: _provider,
|
||||
thinkingOptions,
|
||||
defaultThinkingOptionId,
|
||||
...modelFields
|
||||
}: AgentModelDefinition,
|
||||
thinkingSets: ProviderSnapshotThinkingSet[],
|
||||
thinkingSetIndexes: Map<string, number>,
|
||||
): CompactProviderSnapshotModel {
|
||||
const compact: CompactProviderSnapshotModel = modelFields;
|
||||
|
||||
if (thinkingOptions === undefined) {
|
||||
if (defaultThinkingOptionId !== undefined) {
|
||||
compact.defaultThinkingOptionId = defaultThinkingOptionId;
|
||||
}
|
||||
return compact;
|
||||
}
|
||||
|
||||
const thinkingSet: ProviderSnapshotThinkingSet = {
|
||||
options: thinkingOptions,
|
||||
...(defaultThinkingOptionId !== undefined ? { defaultOptionId: defaultThinkingOptionId } : {}),
|
||||
};
|
||||
const key = thinkingSetKey(thinkingSet);
|
||||
const existingIndex = thinkingSetIndexes.get(key);
|
||||
if (existingIndex !== undefined) {
|
||||
compact.thinkingSet = existingIndex;
|
||||
return compact;
|
||||
}
|
||||
|
||||
const index = thinkingSets.length;
|
||||
thinkingSets.push(thinkingSet);
|
||||
thinkingSetIndexes.set(key, index);
|
||||
compact.thinkingSet = index;
|
||||
return compact;
|
||||
}
|
||||
|
||||
export function compactProviderSnapshot(entries: ProviderSnapshotEntry[]): CompactProviderSnapshot {
|
||||
const thinkingSets: ProviderSnapshotThinkingSet[] = [];
|
||||
const thinkingSetIndexes = new Map<string, number>();
|
||||
const compactEntries = entries.map((entry): CompactProviderSnapshotEntry => {
|
||||
if (entry.models === undefined) {
|
||||
return entry;
|
||||
}
|
||||
return {
|
||||
...entry,
|
||||
models: entry.models.map((model) => compactModel(model, thinkingSets, thinkingSetIndexes)),
|
||||
};
|
||||
});
|
||||
return { entries: compactEntries, thinkingSets };
|
||||
}
|
||||
|
||||
function expandModel(
|
||||
provider: string,
|
||||
model: CompactProviderSnapshotModel,
|
||||
thinkingSets: ProviderSnapshotThinkingSet[],
|
||||
): AgentModelDefinition {
|
||||
const { thinkingSet: thinkingSetIndex, ...modelFields } = model;
|
||||
if (thinkingSetIndex === undefined) {
|
||||
return { provider, ...modelFields };
|
||||
}
|
||||
|
||||
const thinkingSet = thinkingSets[thinkingSetIndex];
|
||||
if (!thinkingSet) {
|
||||
throw new RangeError(`Provider snapshot references missing thinking set ${thinkingSetIndex}`);
|
||||
}
|
||||
return {
|
||||
provider,
|
||||
...modelFields,
|
||||
thinkingOptions: thinkingSet.options,
|
||||
...(thinkingSet.defaultOptionId !== undefined
|
||||
? { defaultThinkingOptionId: thinkingSet.defaultOptionId }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function expandProviderSnapshot(snapshot: CompactProviderSnapshot): ProviderSnapshotEntry[] {
|
||||
return snapshot.entries.map((entry): ProviderSnapshotEntry => {
|
||||
const { models, ...entryFields } = entry;
|
||||
if (models === undefined) return entryFields;
|
||||
return {
|
||||
...entryFields,
|
||||
models: models.map((model) =>
|
||||
expandModel(entryFields.provider, model, snapshot.thinkingSets),
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user