mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Preserve app-server metadata snapshots atomically
This commit is contained in:
parent
ca62b40929
commit
2ca7dc42d2
2 changed files with 67 additions and 21 deletions
|
|
@ -183,23 +183,14 @@ export class AppServerQueryCache {
|
|||
this.client.invalidateQueries({ queryKey: appServerModelsQueryKey(refreshContext) }),
|
||||
]);
|
||||
const metadata = await this.client.fetchQuery(this.appServerMetadataQueryOptions(refreshContext, options));
|
||||
return this.writeAppServerMetadata(refreshContext, metadata);
|
||||
return cloneSharedServerMetadata(metadata);
|
||||
}
|
||||
|
||||
writeAppServerMetadata(context: AppServerQueryContext, metadata: SharedServerMetadata): SharedServerMetadata | null {
|
||||
if (!appServerQueryContextIsComplete(context)) return null;
|
||||
const previous = this.appServerMetadataSnapshot(context);
|
||||
const probes = metadata.serverDiagnostics.probes;
|
||||
const next = cloneSharedServerMetadata({
|
||||
...metadata,
|
||||
availableModels: probes.models.status === "ok" ? metadata.availableModels : (this.modelsSnapshot(context) ?? []),
|
||||
availableSkills: probes.skills.status === "ok" ? metadata.availableSkills : (previous?.availableSkills ?? []),
|
||||
availablePermissionProfiles:
|
||||
probes.permissionProfiles.status === "ok" ? metadata.availablePermissionProfiles : (previous?.availablePermissionProfiles ?? []),
|
||||
rateLimit: probes.rateLimits.status === "ok" ? metadata.rateLimit : (previous?.rateLimit ?? null),
|
||||
});
|
||||
const next = metadataWithLastKnownGood(metadata, this.appServerMetadataSnapshot(context), this.modelsSnapshot(context));
|
||||
this.client.setQueryData(appServerMetadataQueryKey(context), cloneSharedServerMetadata(next));
|
||||
if (probes.models.status === "ok") {
|
||||
if (metadata.serverDiagnostics.probes.models.status === "ok") {
|
||||
this.client.setQueryData(appServerModelsQueryKey(context), cloneModelMetadata(next.availableModels));
|
||||
}
|
||||
return cloneSharedServerMetadata(next);
|
||||
|
|
@ -273,6 +264,7 @@ export class AppServerQueryCache {
|
|||
return {
|
||||
queryKey: appServerMetadataQueryKey(refreshContext),
|
||||
queryFn: async (): Promise<SharedServerMetadata> => {
|
||||
const previous = this.appServerMetadataSnapshot(refreshContext);
|
||||
return this.runWithClient(refreshContext, async (client) => {
|
||||
const runtimeConfig = runtimeConfigSnapshotFromAppServerConfig(await readEffectiveConfig(client, refreshContext.vaultPath));
|
||||
const [models, skills, permissionProfiles, rateLimit] = await Promise.all([
|
||||
|
|
@ -283,16 +275,20 @@ export class AppServerQueryCache {
|
|||
]);
|
||||
const diagnostics = [models.probe, skills.probe, permissionProfiles.probe, rateLimit.probe].reduce(
|
||||
(current, probe) => diagnosticsWithProbe(current, probe),
|
||||
this.appServerMetadataSnapshot(refreshContext)?.serverDiagnostics ?? createServerDiagnostics(),
|
||||
previous?.serverDiagnostics ?? createServerDiagnostics(),
|
||||
);
|
||||
return metadataWithLastKnownGood(
|
||||
{
|
||||
runtimeConfig,
|
||||
availableModels: models.value,
|
||||
availableSkills: skills.value,
|
||||
availablePermissionProfiles: permissionProfiles.value,
|
||||
rateLimit: rateLimit.value,
|
||||
serverDiagnostics: diagnostics,
|
||||
},
|
||||
previous,
|
||||
this.modelsSnapshot(refreshContext),
|
||||
);
|
||||
return {
|
||||
runtimeConfig,
|
||||
availableModels: models.value,
|
||||
availableSkills: skills.value,
|
||||
availablePermissionProfiles: permissionProfiles.value,
|
||||
rateLimit: rateLimit.value,
|
||||
serverDiagnostics: diagnostics,
|
||||
};
|
||||
});
|
||||
},
|
||||
staleTime: APP_SERVER_METADATA_STALE_TIME_MS,
|
||||
|
|
@ -381,6 +377,22 @@ export class AppServerQueryCache {
|
|||
}
|
||||
}
|
||||
|
||||
function metadataWithLastKnownGood(
|
||||
metadata: SharedServerMetadata,
|
||||
previous: SharedServerMetadata | null,
|
||||
models: readonly ModelMetadata[] | null,
|
||||
): SharedServerMetadata {
|
||||
const probes = metadata.serverDiagnostics.probes;
|
||||
return cloneSharedServerMetadata({
|
||||
...metadata,
|
||||
availableModels: probes.models.status === "ok" ? metadata.availableModels : (models ?? previous?.availableModels ?? []),
|
||||
availableSkills: probes.skills.status === "ok" ? metadata.availableSkills : (previous?.availableSkills ?? []),
|
||||
availablePermissionProfiles:
|
||||
probes.permissionProfiles.status === "ok" ? metadata.availablePermissionProfiles : (previous?.availablePermissionProfiles ?? []),
|
||||
rateLimit: probes.rateLimits.status === "ok" ? metadata.rateLimit : (previous?.rateLimit ?? null),
|
||||
});
|
||||
}
|
||||
|
||||
function createAppServerQueryClient(): QueryClient {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
|
|
|
|||
|
|
@ -303,6 +303,40 @@ describe("AppServerQueryCache", () => {
|
|||
expect(cache.modelsSnapshot(context)?.map((model) => model.model)).toEqual(["gpt-cached"]);
|
||||
});
|
||||
|
||||
it("keeps every last-known-good resource through the full metadata refresh path", async () => {
|
||||
const context = cacheContext();
|
||||
const cache = cacheWithRequestHandlers({
|
||||
"config/read": vi.fn().mockResolvedValue({}),
|
||||
"model/list": vi.fn().mockRejectedValue(new Error("models offline")),
|
||||
"skills/list": vi.fn().mockRejectedValue(new Error("skills offline")),
|
||||
"permissionProfile/list": vi.fn().mockRejectedValue(new Error("profiles offline")),
|
||||
"account/rateLimits/read": vi.fn().mockRejectedValue(new Error("limits offline")),
|
||||
});
|
||||
cache.writeAppServerMetadata(
|
||||
context,
|
||||
metadata({
|
||||
availableModels: [modelMetadata("gpt-cached")],
|
||||
availableSkills: [skillMetadata("cached-skill")],
|
||||
availablePermissionProfiles: [permissionProfile(":cached")],
|
||||
rateLimit: rateLimit(17),
|
||||
}),
|
||||
);
|
||||
|
||||
const refreshed = await cache.refreshAppServerMetadata(context);
|
||||
|
||||
expect(refreshed?.availableModels.map((model) => model.model)).toEqual(["gpt-cached"]);
|
||||
expect(refreshed?.availableSkills.map((skill) => skill.name)).toEqual(["cached-skill"]);
|
||||
expect(refreshed?.availablePermissionProfiles.map((profile) => profile.id)).toEqual([":cached"]);
|
||||
expect(refreshed?.rateLimit?.primary?.usedPercent).toBe(17);
|
||||
expect(refreshed?.serverDiagnostics.probes).toMatchObject({
|
||||
models: { status: "failed" },
|
||||
skills: { status: "failed" },
|
||||
permissionProfiles: { status: "failed" },
|
||||
rateLimits: { status: "failed" },
|
||||
});
|
||||
expect(cache.appServerMetadataSnapshot(context)).toEqual(refreshed);
|
||||
});
|
||||
|
||||
it("clears thread list snapshots by context", () => {
|
||||
const cache = new AppServerQueryCache();
|
||||
const context = cacheContext();
|
||||
|
|
|
|||
Loading…
Reference in a new issue