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:
@@ -2,3 +2,6 @@ import Constants from "expo-constants";
|
||||
|
||||
/** F-Droid build without proprietary camera or notification dependencies. */
|
||||
export const isFdroidBuild = Constants.expoConfig?.extra?.fdroidBuild === true;
|
||||
|
||||
/** Production-like Android build with local profiling enabled. */
|
||||
export const isProfileBuild = Constants.expoConfig?.extra?.profileBuild === true;
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
createSetAgentInitializing,
|
||||
refreshAgentInitializationTimeout,
|
||||
} from "@/hooks/use-agent-initialization";
|
||||
import { prefetchProvidersSnapshot } from "@/hooks/use-providers-snapshot";
|
||||
import { generateMessageId, type StreamItem } from "@/types/stream";
|
||||
import {
|
||||
createSessionAgentStreamReducerQueue,
|
||||
@@ -494,19 +493,6 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
||||
});
|
||||
}, [client, serverId, updateSessionServerInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverInfo = client.getLastServerInfoMessage();
|
||||
if (!serverInfo?.features?.providersSnapshot) {
|
||||
return;
|
||||
}
|
||||
|
||||
prefetchProvidersSnapshot(serverId, client);
|
||||
}, [client, isConnected, serverId]);
|
||||
|
||||
useEffect(() => {
|
||||
const unregister = voiceRuntime?.registerSession({
|
||||
serverId,
|
||||
@@ -739,19 +725,6 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
||||
viewedTimelineSyncRef.current?.setConnected(isConnected);
|
||||
}, [isConnected]);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
getHostRuntimeStore().subscribeAgentStoppedRunning(serverId, (agentId) => {
|
||||
const session = useSessionStore.getState().sessions[serverId];
|
||||
const timeline = selectAgentTimelineState(session, agentId);
|
||||
if (timeline.status === "synced" && timeline.newer === "available") {
|
||||
return;
|
||||
}
|
||||
viewedTimelineSyncRef.current?.reconcileAgent(agentId);
|
||||
}),
|
||||
[serverId],
|
||||
);
|
||||
|
||||
// Daemon message handlers - directly update Zustand store
|
||||
useEffect(() => {
|
||||
const agentStreamReducerQueue = createSessionAgentStreamReducerQueue({
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types";
|
||||
import { compactProviderSnapshot } from "@getpaseo/protocol/provider-snapshot-codec";
|
||||
import { createProviderSnapshotCache } from "./provider-snapshot-cache";
|
||||
|
||||
function createStorage() {
|
||||
const values = new Map<string, string>();
|
||||
return {
|
||||
values,
|
||||
async getItem(key: string) {
|
||||
return values.get(key) ?? null;
|
||||
},
|
||||
async setItem(key: string, value: string) {
|
||||
values.set(key, value);
|
||||
},
|
||||
async removeItem(key: string) {
|
||||
values.delete(key);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("provider snapshot cache", () => {
|
||||
it("round-trips compact snapshots with shared thinking option references", async () => {
|
||||
const thinkingOptions = [{ id: "high", label: "High", isDefault: true }];
|
||||
const entries: ProviderSnapshotEntry[] = [
|
||||
{
|
||||
provider: "pi",
|
||||
status: "ready",
|
||||
enabled: true,
|
||||
models: ["one", "two"].map((id) => ({
|
||||
provider: "pi",
|
||||
id,
|
||||
label: id,
|
||||
thinkingOptions,
|
||||
defaultThinkingOptionId: "high",
|
||||
})),
|
||||
},
|
||||
];
|
||||
const storage = createStorage();
|
||||
const cache = createProviderSnapshotCache(storage);
|
||||
|
||||
await cache.write({
|
||||
serverId: "server-1",
|
||||
cwd: "/repo",
|
||||
hash: "snapshot-hash",
|
||||
generatedAt: "2026-08-04T00:00:00.000Z",
|
||||
compactSnapshot: compactProviderSnapshot(entries),
|
||||
});
|
||||
const cached = await cache.read("server-1", "/repo");
|
||||
|
||||
expect(cached?.entries).toEqual(entries);
|
||||
expect(cached?.entries[0]?.models?.[0]?.thinkingOptions).toBe(
|
||||
cached?.entries[0]?.models?.[1]?.thinkingOptions,
|
||||
);
|
||||
});
|
||||
|
||||
it("discards an invalid cache record", async () => {
|
||||
const storage = createStorage();
|
||||
const cache = createProviderSnapshotCache(storage);
|
||||
storage.values.set('@paseo/provider-snapshot/v1:["server-1","/repo"]', "not json");
|
||||
|
||||
await expect(cache.read("server-1", "/repo")).resolves.toBeNull();
|
||||
expect(storage.values.size).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types";
|
||||
import {
|
||||
expandProviderSnapshot,
|
||||
type CompactProviderSnapshot,
|
||||
} from "@getpaseo/protocol/provider-snapshot-codec";
|
||||
import { CompactProviderSnapshotSchema } from "@getpaseo/protocol/messages";
|
||||
|
||||
const CACHE_VERSION = 1;
|
||||
const CACHE_KEY_PREFIX = "@paseo/provider-snapshot/v1";
|
||||
|
||||
interface ProviderSnapshotStorage {
|
||||
getItem(key: string): Promise<string | null>;
|
||||
setItem(key: string, value: string): Promise<void>;
|
||||
removeItem(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
interface StoredProviderSnapshot {
|
||||
version: typeof CACHE_VERSION;
|
||||
hash: string;
|
||||
generatedAt: string;
|
||||
compactSnapshot: CompactProviderSnapshot;
|
||||
}
|
||||
|
||||
export interface CachedProviderSnapshot extends StoredProviderSnapshot {
|
||||
entries: ProviderSnapshotEntry[];
|
||||
}
|
||||
|
||||
export interface ProviderSnapshotCache {
|
||||
read(serverId: string, cwd: string | null): Promise<CachedProviderSnapshot | null>;
|
||||
write(input: {
|
||||
serverId: string;
|
||||
cwd: string | null;
|
||||
hash: string;
|
||||
generatedAt: string;
|
||||
compactSnapshot: CompactProviderSnapshot;
|
||||
}): Promise<void>;
|
||||
}
|
||||
|
||||
export class ProviderSnapshotCacheMissError extends Error {
|
||||
constructor() {
|
||||
super("Daemon returned not-modified without a cached provider snapshot");
|
||||
this.name = "ProviderSnapshotCacheMissError";
|
||||
}
|
||||
}
|
||||
|
||||
function cacheKey(serverId: string, cwd: string | null): string {
|
||||
return `${CACHE_KEY_PREFIX}:${JSON.stringify([serverId, cwd])}`;
|
||||
}
|
||||
|
||||
function parseStoredProviderSnapshot(value: string): StoredProviderSnapshot | null {
|
||||
const parsed: unknown = JSON.parse(value);
|
||||
if (typeof parsed !== "object" || parsed === null) return null;
|
||||
const version = Reflect.get(parsed, "version");
|
||||
const hash = Reflect.get(parsed, "hash");
|
||||
const generatedAt = Reflect.get(parsed, "generatedAt");
|
||||
const compactSnapshot = CompactProviderSnapshotSchema.safeParse(
|
||||
Reflect.get(parsed, "compactSnapshot"),
|
||||
);
|
||||
if (
|
||||
version !== CACHE_VERSION ||
|
||||
typeof hash !== "string" ||
|
||||
typeof generatedAt !== "string" ||
|
||||
!compactSnapshot.success
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
version,
|
||||
hash,
|
||||
generatedAt,
|
||||
compactSnapshot: compactSnapshot.data,
|
||||
};
|
||||
}
|
||||
|
||||
async function discardCachedSnapshot(storage: ProviderSnapshotStorage, key: string): Promise<void> {
|
||||
try {
|
||||
await storage.removeItem(key);
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error)) throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function createProviderSnapshotCache(
|
||||
storage: ProviderSnapshotStorage,
|
||||
): ProviderSnapshotCache {
|
||||
return {
|
||||
async read(serverId, cwd) {
|
||||
const key = cacheKey(serverId, cwd);
|
||||
let value: string | null;
|
||||
try {
|
||||
value = await storage.getItem(key);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) return null;
|
||||
throw error;
|
||||
}
|
||||
if (value === null) return null;
|
||||
try {
|
||||
const stored = parseStoredProviderSnapshot(value);
|
||||
if (!stored) {
|
||||
await discardCachedSnapshot(storage, key);
|
||||
return null;
|
||||
}
|
||||
return { ...stored, entries: expandProviderSnapshot(stored.compactSnapshot) };
|
||||
} catch (error) {
|
||||
if (!(error instanceof SyntaxError) && !(error instanceof RangeError)) {
|
||||
throw error;
|
||||
}
|
||||
await discardCachedSnapshot(storage, key);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
async write(input) {
|
||||
const stored: StoredProviderSnapshot = {
|
||||
version: CACHE_VERSION,
|
||||
hash: input.hash,
|
||||
generatedAt: input.generatedAt,
|
||||
compactSnapshot: input.compactSnapshot,
|
||||
};
|
||||
try {
|
||||
await storage.setItem(cacheKey(input.serverId, input.cwd), JSON.stringify(stored));
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error)) throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const providerSnapshotCache = createProviderSnapshotCache(AsyncStorage);
|
||||
@@ -21,11 +21,13 @@ export function providersSnapshotQueryKey(serverId: string | null, cwd?: string
|
||||
export function providersSnapshotRequestOptions(input: {
|
||||
cwd?: string | null;
|
||||
providers?: AgentProvider[];
|
||||
ifNoneMatch?: string;
|
||||
}) {
|
||||
const normalizedCwd = normalizeProvidersSnapshotCwd(input.cwd);
|
||||
return {
|
||||
...(normalizedCwd ? { cwd: normalizedCwd } : {}),
|
||||
...(input.providers ? { providers: input.providers } : {}),
|
||||
...(input.ifNoneMatch ? { ifNoneMatch: input.ifNoneMatch } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,12 @@ import { agentCommandsQueryRoot } from "@/hooks/agent-commands-query";
|
||||
import { orderCheckoutDiffFiles } from "@/git/diff-order";
|
||||
import { daemonConfigQueryKey } from "@/data/daemon-config";
|
||||
import { daemonPairingOfferQueryKey } from "@/data/daemon-pairing";
|
||||
import { providersSnapshotQueryKey, providersSnapshotQueryRoot } from "@/data/providers-snapshot";
|
||||
import { providerSnapshotCache, type ProviderSnapshotCache } from "@/data/provider-snapshot-cache";
|
||||
import {
|
||||
normalizeProvidersSnapshotCwd,
|
||||
providersSnapshotQueryKey,
|
||||
providersSnapshotQueryRoot,
|
||||
} from "@/data/providers-snapshot";
|
||||
|
||||
type ProvidersSnapshotUpdateMessage = Extract<
|
||||
SessionOutboundMessage,
|
||||
@@ -184,6 +189,7 @@ export function applyProvidersSnapshotUpdate(input: {
|
||||
serverId: string;
|
||||
queryClient: QueryClient;
|
||||
message: ProvidersSnapshotUpdate;
|
||||
cache?: ProviderSnapshotCache;
|
||||
}): void {
|
||||
if (input.message.type !== "providers_snapshot_update") {
|
||||
return;
|
||||
@@ -194,6 +200,16 @@ export function applyProvidersSnapshotUpdate(input: {
|
||||
generatedAt: input.message.payload.generatedAt,
|
||||
requestId: "providers_snapshot_update",
|
||||
});
|
||||
const { compactSnapshot, snapshotHash } = input.message.payload;
|
||||
if (compactSnapshot && snapshotHash) {
|
||||
void (input.cache ?? providerSnapshotCache).write({
|
||||
serverId: input.serverId,
|
||||
cwd: normalizeProvidersSnapshotCwd(input.message.payload.cwd),
|
||||
hash: snapshotHash,
|
||||
generatedAt: input.message.payload.generatedAt,
|
||||
compactSnapshot,
|
||||
});
|
||||
}
|
||||
void input.queryClient.invalidateQueries({
|
||||
queryKey: agentCommandsQueryRoot(input.serverId),
|
||||
exact: false,
|
||||
|
||||
@@ -3,6 +3,8 @@ import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types";
|
||||
import { compactProviderSnapshot } from "@getpaseo/protocol/provider-snapshot-codec";
|
||||
import type { CachedProviderSnapshot, ProviderSnapshotCache } from "@/data/provider-snapshot-cache";
|
||||
import { draftAgentCommandsQueryKey } from "@/hooks/agent-commands-query";
|
||||
import { applyProvidersSnapshotUpdate, type ProvidersSnapshotUpdate } from "@/data/push-router";
|
||||
import {
|
||||
@@ -79,6 +81,21 @@ function codexEntry(
|
||||
const readyCodexModel = { provider: "codex", id: "gpt-5.4", label: "GPT-5.4" } as const;
|
||||
const serverId = "server-1";
|
||||
|
||||
function createCache(initial: CachedProviderSnapshot | null = null): ProviderSnapshotCache & {
|
||||
writes: Parameters<ProviderSnapshotCache["write"]>[0][];
|
||||
} {
|
||||
const writes: Parameters<ProviderSnapshotCache["write"]>[0][] = [];
|
||||
return {
|
||||
writes,
|
||||
async read() {
|
||||
return initial;
|
||||
},
|
||||
async write(input) {
|
||||
writes.push(input);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("providersSnapshotQueryKey", () => {
|
||||
it("uses separate keys for home and workspace scopes", () => {
|
||||
expect(providersSnapshotQueryKey(serverId)).toEqual(["providersSnapshot", serverId, "home"]);
|
||||
@@ -95,7 +112,7 @@ describe("fetchProvidersSnapshot", () => {
|
||||
it("sends no cwd for the home scope", async () => {
|
||||
const client = createClient({ snapshots: [providersSnapshot([])] });
|
||||
|
||||
await fetchProvidersSnapshot({ client, cwd: null });
|
||||
await fetchProvidersSnapshot({ client, serverId, cwd: null, cache: createCache() });
|
||||
|
||||
expect(client.getCalls).toEqual([{}]);
|
||||
});
|
||||
@@ -103,10 +120,78 @@ describe("fetchProvidersSnapshot", () => {
|
||||
it("sends the workspace cwd for the workspace scope", async () => {
|
||||
const client = createClient({ snapshots: [providersSnapshot([])] });
|
||||
|
||||
await fetchProvidersSnapshot({ client, cwd: "/repo-a" });
|
||||
await fetchProvidersSnapshot({
|
||||
client,
|
||||
serverId,
|
||||
cwd: "/repo-a",
|
||||
cache: createCache(),
|
||||
});
|
||||
|
||||
expect(client.getCalls).toEqual([{ cwd: "/repo-a" }]);
|
||||
});
|
||||
|
||||
it("reuses a cached snapshot when the daemon reports its hash unchanged", async () => {
|
||||
const entries = [codexEntry("ready", [readyCodexModel])];
|
||||
const compactSnapshot = compactProviderSnapshot(entries);
|
||||
const cache = createCache({
|
||||
version: 1,
|
||||
hash: "snapshot-hash",
|
||||
generatedAt: "2026-01-01T00:00:00.000Z",
|
||||
compactSnapshot,
|
||||
entries,
|
||||
});
|
||||
const client = createClient({
|
||||
snapshots: [
|
||||
{
|
||||
entries: [],
|
||||
snapshotHash: "snapshot-hash",
|
||||
notModified: true,
|
||||
generatedAt: "2026-01-02T00:00:00.000Z",
|
||||
requestId: "snapshot-2",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const snapshot = await fetchProvidersSnapshot({
|
||||
client,
|
||||
serverId,
|
||||
cwd: "/repo-a",
|
||||
cache,
|
||||
});
|
||||
|
||||
expect(client.getCalls).toEqual([{ cwd: "/repo-a", ifNoneMatch: "snapshot-hash" }]);
|
||||
expect(snapshot.entries).toBe(entries);
|
||||
expect(cache.writes).toEqual([]);
|
||||
});
|
||||
|
||||
it("persists a changed compact snapshot for the next launch", async () => {
|
||||
const entries = [codexEntry("ready", [readyCodexModel])];
|
||||
const compactSnapshot = compactProviderSnapshot(entries);
|
||||
const cache = createCache();
|
||||
const client = createClient({
|
||||
snapshots: [
|
||||
{
|
||||
entries,
|
||||
compactSnapshot,
|
||||
snapshotHash: "next-hash",
|
||||
generatedAt: "2026-01-02T00:00:00.000Z",
|
||||
requestId: "snapshot-2",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await fetchProvidersSnapshot({ client, serverId, cwd: "/repo-a", cache });
|
||||
|
||||
expect(cache.writes).toEqual([
|
||||
{
|
||||
serverId,
|
||||
cwd: "/repo-a",
|
||||
hash: "next-hash",
|
||||
generatedAt: "2026-01-02T00:00:00.000Z",
|
||||
compactSnapshot,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("refreshAndApplyProvidersSnapshot", () => {
|
||||
@@ -127,6 +212,7 @@ describe("refreshAndApplyProvidersSnapshot", () => {
|
||||
serverId,
|
||||
cwd: null,
|
||||
providers: ["codex"],
|
||||
cache: createCache(),
|
||||
});
|
||||
|
||||
expect(client.refreshCalls).toEqual([{ providers: ["codex"] }]);
|
||||
@@ -147,6 +233,7 @@ describe("refreshAndApplyProvidersSnapshot", () => {
|
||||
serverId,
|
||||
cwd: "/repo-a",
|
||||
providers: ["codex"],
|
||||
cache: createCache(),
|
||||
});
|
||||
|
||||
expect(client.refreshCalls).toEqual([{ cwd: "/repo-a", providers: ["codex"] }]);
|
||||
@@ -166,6 +253,7 @@ describe("refreshAndApplyProvidersSnapshot", () => {
|
||||
queryClient,
|
||||
serverId,
|
||||
cwd: null,
|
||||
cache: createCache(),
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -186,6 +274,7 @@ describe("refreshAndApplyProvidersSnapshot", () => {
|
||||
queryClient,
|
||||
serverId,
|
||||
cwd: "/repo-a",
|
||||
cache: createCache(),
|
||||
});
|
||||
|
||||
expect(queryClient.getQueryState(providersSnapshotQueryKey(serverId))?.isInvalidated).toBe(
|
||||
@@ -209,6 +298,7 @@ describe("refreshAndApplyProvidersSnapshot", () => {
|
||||
queryClient,
|
||||
serverId,
|
||||
cwd: "/repo-a",
|
||||
cache: createCache(),
|
||||
});
|
||||
|
||||
expect(queryClient.getQueryState(commandsKey)?.isInvalidated).toBe(true);
|
||||
@@ -266,6 +356,27 @@ describe("applyProvidersSnapshotUpdate", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("persists compact push updates", () => {
|
||||
const entries = [codexEntry("ready", [readyCodexModel])];
|
||||
const compactSnapshot = compactProviderSnapshot(entries);
|
||||
const cache = createCache();
|
||||
const message = updateMessage(entries, "/repo-a");
|
||||
message.payload.compactSnapshot = compactSnapshot;
|
||||
message.payload.snapshotHash = "push-hash";
|
||||
|
||||
applyProvidersSnapshotUpdate({ serverId, queryClient, message, cache });
|
||||
|
||||
expect(cache.writes).toEqual([
|
||||
{
|
||||
serverId,
|
||||
cwd: "/repo-a",
|
||||
hash: "push-hash",
|
||||
generatedAt: "2026-01-01T00:00:01.000Z",
|
||||
compactSnapshot,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("applies Windows daemon updates to app-normalized workspace paths", () => {
|
||||
const workspaceCwd = "C:/Users/Ezekiel Bulver/project";
|
||||
const daemonCwd = "C:\\Users\\Ezekiel Bulver\\project";
|
||||
|
||||
@@ -8,6 +8,11 @@ import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { useReplicaQuery } from "@/data/query";
|
||||
import { queryClient as singletonQueryClient } from "@/data/query-client";
|
||||
import {
|
||||
providerSnapshotCache,
|
||||
ProviderSnapshotCacheMissError,
|
||||
type ProviderSnapshotCache,
|
||||
} from "@/data/provider-snapshot-cache";
|
||||
import { agentCommandsQueryRoot } from "@/hooks/agent-commands-query";
|
||||
import {
|
||||
isProvidersSnapshotHomeScope,
|
||||
@@ -29,9 +34,31 @@ export type ProvidersSnapshotClient = Pick<
|
||||
|
||||
export async function fetchProvidersSnapshot(input: {
|
||||
client: ProvidersSnapshotClient;
|
||||
serverId: string;
|
||||
cwd: string | null;
|
||||
cache?: ProviderSnapshotCache;
|
||||
}): Promise<GetProvidersSnapshotResult> {
|
||||
return input.client.getProvidersSnapshot(providersSnapshotRequestOptions({ cwd: input.cwd }));
|
||||
const cache = input.cache ?? providerSnapshotCache;
|
||||
const cached = await cache.read(input.serverId, input.cwd);
|
||||
const snapshot = await input.client.getProvidersSnapshot(
|
||||
providersSnapshotRequestOptions({ cwd: input.cwd, ifNoneMatch: cached?.hash }),
|
||||
);
|
||||
if (snapshot.notModified) {
|
||||
if (!cached) {
|
||||
throw new ProviderSnapshotCacheMissError();
|
||||
}
|
||||
return { ...snapshot, entries: cached.entries };
|
||||
}
|
||||
if (snapshot.compactSnapshot && snapshot.snapshotHash) {
|
||||
await cache.write({
|
||||
serverId: input.serverId,
|
||||
cwd: input.cwd,
|
||||
hash: snapshot.snapshotHash,
|
||||
generatedAt: snapshot.generatedAt,
|
||||
compactSnapshot: snapshot.compactSnapshot,
|
||||
});
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
export async function refreshAndApplyProvidersSnapshot(input: {
|
||||
@@ -40,11 +67,17 @@ export async function refreshAndApplyProvidersSnapshot(input: {
|
||||
serverId: string;
|
||||
cwd: string | null;
|
||||
providers?: AgentProvider[];
|
||||
cache?: ProviderSnapshotCache;
|
||||
}): Promise<RefreshProvidersSnapshotResult> {
|
||||
const refreshResult = await input.client.refreshProvidersSnapshot(
|
||||
providersSnapshotRequestOptions({ cwd: input.cwd, providers: input.providers }),
|
||||
);
|
||||
const snapshot = await fetchProvidersSnapshot({ client: input.client, cwd: input.cwd });
|
||||
const snapshot = await fetchProvidersSnapshot({
|
||||
client: input.client,
|
||||
serverId: input.serverId,
|
||||
cwd: input.cwd,
|
||||
cache: input.cache,
|
||||
});
|
||||
input.queryClient.setQueryData(providersSnapshotQueryKey(input.serverId, input.cwd), snapshot);
|
||||
void input.queryClient.invalidateQueries({
|
||||
queryKey: agentCommandsQueryRoot(input.serverId),
|
||||
@@ -113,10 +146,10 @@ export function useProvidersSnapshot(
|
||||
enabled: Boolean(enabled && supportsSnapshot && serverId && client && isConnected),
|
||||
pushEvent: "providers_snapshot_update",
|
||||
queryFn: async () => {
|
||||
if (!client) {
|
||||
if (!client || !serverId) {
|
||||
throw new Error(t("workspace.terminal.hostDisconnected"));
|
||||
}
|
||||
return fetchProvidersSnapshot({ client, cwd });
|
||||
return fetchProvidersSnapshot({ client, serverId, cwd });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -180,6 +213,6 @@ export function prefetchProvidersSnapshot(
|
||||
void singletonQueryClient.prefetchQuery({
|
||||
queryKey,
|
||||
staleTime: Infinity,
|
||||
queryFn: () => fetchProvidersSnapshot({ client, cwd }),
|
||||
queryFn: () => fetchProvidersSnapshot({ client, serverId, cwd }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { DaemonClientTrace } from "@getpaseo/client/internal/daemon-client";
|
||||
import { requireOptionalNativeModule } from "expo-modules-core";
|
||||
import { isProfileBuild } from "@/constants/build-profile";
|
||||
|
||||
interface PaseoNativeTraceModule {
|
||||
beginSection(name: string): void;
|
||||
endSection(): void;
|
||||
}
|
||||
|
||||
const traceModule = requireOptionalNativeModule<PaseoNativeTraceModule>("PaseoNativeTrace");
|
||||
|
||||
export const nativePerformanceTrace: DaemonClientTrace = {
|
||||
isEnabled() {
|
||||
return isProfileBuild && traceModule !== null;
|
||||
},
|
||||
beginSection(name, args) {
|
||||
traceModule?.beginSection(formatSectionName(name, args));
|
||||
},
|
||||
endSection() {
|
||||
traceModule?.endSection();
|
||||
},
|
||||
};
|
||||
|
||||
function formatSectionName(name: string, args?: Record<string, string>): string {
|
||||
if (!args) {
|
||||
return name;
|
||||
}
|
||||
const fields = Object.entries(args).map(([key, value]) => `${key}=${value}`);
|
||||
return `${name}|${fields.join(";")}`;
|
||||
}
|
||||
|
||||
export function traceInstant(name: string, args?: Record<string, string>): void {
|
||||
if (!nativePerformanceTrace.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
nativePerformanceTrace.beginSection(name, args);
|
||||
nativePerformanceTrace.endSection();
|
||||
}
|
||||
@@ -56,6 +56,7 @@ import { resolveComposerAttachmentSubmitFormat } from "@/composer/attachments/su
|
||||
import { encodeImages } from "@/utils/encode-images";
|
||||
import { DirectorySync, type RefreshAgentDirectoryResult } from "@/runtime/directory-sync";
|
||||
import { ReplicaCache } from "@/runtime/replica-cache";
|
||||
import { nativePerformanceTrace } from "@/performance/native-trace";
|
||||
|
||||
export type HostRuntimeConnectionStatus = "idle" | "connecting" | "online" | "offline" | "error";
|
||||
export type HostRegistryStatus = "loading" | "ready";
|
||||
@@ -483,6 +484,7 @@ function createDefaultDeps(): HostRuntimeControllerDeps {
|
||||
appVersion: resolveAppVersion() ?? undefined,
|
||||
runtimeGeneration,
|
||||
capabilities: appCapabilities,
|
||||
trace: nativePerformanceTrace,
|
||||
};
|
||||
if (connection.type === "directSocket" || connection.type === "directPipe") {
|
||||
return new DaemonClient({
|
||||
@@ -521,6 +523,7 @@ function createDefaultDeps(): HostRuntimeControllerDeps {
|
||||
...(host.serverId ? { serverId: host.serverId } : {}),
|
||||
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
||||
capabilities: appCapabilities,
|
||||
trace: nativePerformanceTrace,
|
||||
}),
|
||||
getClientId: () => getOrCreateClientId(),
|
||||
mountClientHandlers: ({ client, host }) => {
|
||||
@@ -1341,7 +1344,6 @@ interface AgentDirectoryRefreshInput {
|
||||
export class HostRuntimeStore {
|
||||
private controllers = new Map<string, HostRuntimeController>();
|
||||
private serverListeners = new Map<string, Set<() => void>>();
|
||||
private agentStoppedRunningListeners = new Map<string, Set<(agentId: string) => void>>();
|
||||
private globalListeners = new Set<() => void>();
|
||||
private hostListListeners = new Set<() => void>();
|
||||
private version = 0;
|
||||
@@ -1589,12 +1591,11 @@ export class HostRuntimeStore {
|
||||
rekeyMap(this.controllers, oldServerId, newServerId);
|
||||
rekeyMap(this.lastConnectionStatusByServer, oldServerId, newServerId);
|
||||
rekeyMap(this.directoryBootstrapInFlight, oldServerId, newServerId);
|
||||
rekeyMap(this.agentStoppedRunningListeners, oldServerId, newServerId);
|
||||
this.replicaCache.reconcileServerId(oldServerId, newServerId);
|
||||
this.directorySyncByServer.get(oldServerId)?.dispose();
|
||||
this.directorySyncByServer.delete(oldServerId);
|
||||
const directory = new DirectorySync(newServerId, {
|
||||
onAgentStoppedRunning: (agentId) => this.onAgentStoppedRunning(newServerId, agentId),
|
||||
onAgentStoppedRunning: (agentId) => this.drainQueuedAgentMessage(newServerId, agentId),
|
||||
markAgentLoading: () => controller.markAgentDirectorySyncLoading(),
|
||||
markAgentReady: () => controller.markAgentDirectorySyncReady(),
|
||||
markAgentError: (error) => controller.markAgentDirectorySyncError(error),
|
||||
@@ -1977,7 +1978,7 @@ export class HostRuntimeStore {
|
||||
this.directorySyncByServer.set(
|
||||
host.serverId,
|
||||
new DirectorySync(host.serverId, {
|
||||
onAgentStoppedRunning: (agentId) => this.onAgentStoppedRunning(host.serverId, agentId),
|
||||
onAgentStoppedRunning: (agentId) => this.drainQueuedAgentMessage(host.serverId, agentId),
|
||||
markAgentLoading: () => controller.markAgentDirectorySyncLoading(),
|
||||
markAgentReady: () => controller.markAgentDirectorySyncReady(),
|
||||
markAgentError: (error) => controller.markAgentDirectorySyncError(error),
|
||||
@@ -2176,18 +2177,6 @@ export class HostRuntimeStore {
|
||||
};
|
||||
}
|
||||
|
||||
subscribeAgentStoppedRunning(serverId: string, listener: (agentId: string) => void): () => void {
|
||||
const listeners = this.agentStoppedRunningListeners.get(serverId) ?? new Set();
|
||||
listeners.add(listener);
|
||||
this.agentStoppedRunningListeners.set(serverId, listeners);
|
||||
return () => {
|
||||
const current = this.agentStoppedRunningListeners.get(serverId);
|
||||
if (!current) return;
|
||||
current.delete(listener);
|
||||
if (current.size === 0) this.agentStoppedRunningListeners.delete(serverId);
|
||||
};
|
||||
}
|
||||
|
||||
subscribeAll(listener: () => void): () => void {
|
||||
this.globalListeners.add(listener);
|
||||
return () => {
|
||||
@@ -2296,13 +2285,6 @@ export class HostRuntimeStore {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
private onAgentStoppedRunning(serverId: string, agentId: string): void {
|
||||
this.drainQueuedAgentMessage(serverId, agentId);
|
||||
for (const listener of this.agentStoppedRunningListeners.get(serverId) ?? []) {
|
||||
listener(agentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let singletonHostRuntimeStore: HostRuntimeStore | null = null;
|
||||
|
||||
@@ -74,6 +74,7 @@ import { ImportSessionSheet } from "@/components/import-session-sheet";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import { selectIsFileExplorerOpen, usePanelStore } from "@/stores/panel-store";
|
||||
import { type ExplorerCheckoutContext } from "@/stores/explorer-checkout-context";
|
||||
import { traceInstant } from "@/performance/native-trace";
|
||||
import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import {
|
||||
collectAllTabs,
|
||||
@@ -938,6 +939,12 @@ export const WorkspaceScreen = memo(function WorkspaceScreen({
|
||||
recoveryAgentId,
|
||||
}: WorkspaceScreenProps) {
|
||||
const navigationFocused = useIsFocused();
|
||||
useEffect(() => {
|
||||
traceInstant("paseo.workspace.mount", { serverId, workspaceId });
|
||||
return () => {
|
||||
traceInstant("paseo.workspace.unmount", { serverId, workspaceId });
|
||||
};
|
||||
}, [serverId, workspaceId]);
|
||||
return (
|
||||
<WorkspaceScreenContent
|
||||
serverId={serverId}
|
||||
|
||||
@@ -190,39 +190,6 @@ test("a gap absorbed by a running tail is recovered after the tail completes", a
|
||||
await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready"));
|
||||
});
|
||||
|
||||
test("an idle reconciliation waits for an in-flight tail and fetches a fresh snapshot", async () => {
|
||||
const world = new TimelineWorld();
|
||||
world.sync.setConnected(true);
|
||||
world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]);
|
||||
const membership = await world.nextMembership();
|
||||
membership.succeed();
|
||||
const initialTail = await world.nextFetch("agent-a");
|
||||
|
||||
world.sync.reconcileAgent("agent-a");
|
||||
|
||||
world.expectNoPendingFetch();
|
||||
initialTail.respond({ hasNewer: false });
|
||||
|
||||
const reconciliation = await world.nextFetch("agent-a");
|
||||
expect(reconciliation.request).toEqual({
|
||||
direction: "tail",
|
||||
limit: 40,
|
||||
projection: "projected",
|
||||
});
|
||||
reconciliation.respond({ hasNewer: false });
|
||||
await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready"));
|
||||
});
|
||||
|
||||
test("does not reconcile an agent outside the viewed membership", () => {
|
||||
const world = new TimelineWorld();
|
||||
world.sync.setConnected(true);
|
||||
|
||||
world.sync.reconcileAgent("agent-a");
|
||||
|
||||
world.expectNoPendingFetch();
|
||||
world.expectNoPendingMembership();
|
||||
});
|
||||
|
||||
test("unchanged visible-set publication does not cancel paged catch-up", async () => {
|
||||
const world = new TimelineWorld();
|
||||
world.sync.setConnected(true);
|
||||
|
||||
@@ -33,7 +33,6 @@ export interface ViewedTimelineSync extends ViewedTimelineUiBridge {
|
||||
setActive(active: boolean): void;
|
||||
setConnected(connected: boolean): void;
|
||||
setDeliveryMode(mode: TimelineDeliveryMode): void;
|
||||
reconcileAgent(agentId: string): void;
|
||||
recoverGap(agentId: string, cursor: { epoch: string; endSeq: number }): void;
|
||||
dispose(): void;
|
||||
}
|
||||
@@ -451,15 +450,6 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed
|
||||
if (deliveryMode === "selective" && connected) void reconcileMembership();
|
||||
else if (connected) startAcknowledgedCatchUps();
|
||||
},
|
||||
reconcileAgent(agentId) {
|
||||
if (!isDesired(agentId)) return;
|
||||
const request = planTimelineTailFetch();
|
||||
if (catchUps.get(agentId)?.status === "running") {
|
||||
pendingCatchUps.set(agentId, request);
|
||||
return;
|
||||
}
|
||||
startCatchUp(agentId, { request, supersede: true });
|
||||
},
|
||||
recoverGap(agentId, cursor) {
|
||||
if (!isDesired(agentId)) return;
|
||||
startCatchUp(agentId, {
|
||||
|
||||
@@ -134,6 +134,27 @@ describe("test-daemon-connection connectToDaemon", () => {
|
||||
expect(probe.createdConfigs()[0]?.password).toBe("shared-secret");
|
||||
});
|
||||
|
||||
it("passes performance tracing into the connected client", async () => {
|
||||
const { connectToDaemon } = await import("./test-daemon-connection");
|
||||
const trace = {
|
||||
isEnabled: () => true,
|
||||
beginSection: vi.fn(),
|
||||
endSection: vi.fn(),
|
||||
};
|
||||
const result = await connectToDaemon(
|
||||
{
|
||||
id: "direct:lan:6767",
|
||||
type: "directTcp",
|
||||
endpoint: "lan:6767",
|
||||
},
|
||||
{ trace },
|
||||
probe.deps,
|
||||
);
|
||||
await result.client.close();
|
||||
|
||||
expect(probe.createdConfigs()[0]?.trace).toBe(trace);
|
||||
});
|
||||
|
||||
it("uses relay TLS from the stored connection", async () => {
|
||||
const { connectToDaemon } = await import("./test-daemon-connection");
|
||||
const tlsResult = await connectToDaemon(
|
||||
|
||||
@@ -97,7 +97,10 @@ export class DaemonConnectionTestError extends Error {
|
||||
export async function buildClientConfig(
|
||||
connection: HostConnection,
|
||||
serverId?: string,
|
||||
options?: { capabilities?: DaemonClientConfig["capabilities"] },
|
||||
options?: {
|
||||
capabilities?: DaemonClientConfig["capabilities"];
|
||||
trace?: DaemonClientConfig["trace"];
|
||||
},
|
||||
deps: Pick<
|
||||
DaemonConnectionDependencies<DaemonProbeClient>,
|
||||
"getClientId" | "resolveAppVersion" | "createLocalTransportFactory" | "buildLocalTransportUrl"
|
||||
@@ -112,6 +115,7 @@ export async function buildClientConfig(
|
||||
suppressSendErrors: true,
|
||||
reconnect: { enabled: false },
|
||||
...(options?.capabilities ? { capabilities: options.capabilities } : {}),
|
||||
...(options?.trace ? { trace: options.trace } : {}),
|
||||
...((connection.type === "directSocket" || connection.type === "directPipe") &&
|
||||
localTransportFactory
|
||||
? { transportFactory: localTransportFactory }
|
||||
@@ -224,6 +228,7 @@ interface ProbeOptions {
|
||||
serverId?: string;
|
||||
timeoutMs?: number;
|
||||
capabilities?: DaemonClientConfig["capabilities"];
|
||||
trace?: DaemonClientConfig["trace"];
|
||||
}
|
||||
|
||||
function resolveTimeout(connection: HostConnection, options?: ProbeOptions): number {
|
||||
|
||||
Reference in New Issue
Block a user