mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Make shared app-server metadata cache resource-level
This commit is contained in:
parent
b547ef5b42
commit
2499ffca38
4 changed files with 184 additions and 33 deletions
|
|
@ -56,15 +56,22 @@ export function applySharedServerMetadata(
|
|||
metadata: SharedServerMetadata,
|
||||
): SharedAppServerState {
|
||||
if (!sharedAppServerCacheContextIsComplete(context)) return state;
|
||||
const clonedMetadata = cloneSharedServerMetadata(metadata);
|
||||
const previous =
|
||||
state.appServerMetadata.kind === "loaded" && sharedAppServerCacheContextMatches(state.appServerMetadata.context, context)
|
||||
? state.appServerMetadata.data
|
||||
: null;
|
||||
const clonedMetadata = mergeSharedServerMetadata(previous, metadata);
|
||||
return {
|
||||
...state,
|
||||
appServerMetadata: { kind: "loaded", context: cloneSharedAppServerCacheContext(context), data: clonedMetadata },
|
||||
availableModels: {
|
||||
kind: "loaded",
|
||||
context: cloneSharedAppServerCacheContext(context),
|
||||
data: cloneModelMetadata(clonedMetadata.availableModels),
|
||||
},
|
||||
availableModels:
|
||||
clonedMetadata.availableModels.length > 0
|
||||
? {
|
||||
kind: "loaded",
|
||||
context: cloneSharedAppServerCacheContext(context),
|
||||
data: cloneModelMetadata(clonedMetadata.availableModels),
|
||||
}
|
||||
: state.availableModels,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -125,6 +132,53 @@ function cloneSharedServerMetadata(metadata: SharedServerMetadata): SharedServer
|
|||
};
|
||||
}
|
||||
|
||||
function mergeSharedServerMetadata(previous: SharedServerMetadata | null, next: SharedServerMetadata): SharedServerMetadata {
|
||||
const clonedNext = cloneSharedServerMetadata(next);
|
||||
const clonedPrevious = previous ? cloneSharedServerMetadata(previous) : emptySharedServerMetadataResourceCache(clonedNext);
|
||||
return {
|
||||
...clonedNext,
|
||||
availableModels: metadataResourceSucceeded(clonedNext, "model/list") ? clonedNext.availableModels : clonedPrevious.availableModels,
|
||||
availableSkills: metadataResourceSucceeded(clonedNext, "skills/list") ? clonedNext.availableSkills : clonedPrevious.availableSkills,
|
||||
rateLimit: metadataResourceSucceeded(clonedNext, "account/rateLimits/read") ? clonedNext.rateLimit : clonedPrevious.rateLimit,
|
||||
serverDiagnostics: mergeServerDiagnostics(clonedPrevious, clonedNext),
|
||||
};
|
||||
}
|
||||
|
||||
function emptySharedServerMetadataResourceCache(metadata: SharedServerMetadata): SharedServerMetadata {
|
||||
return {
|
||||
...metadata,
|
||||
availableModels: [],
|
||||
availableSkills: [],
|
||||
rateLimit: null,
|
||||
serverDiagnostics: {
|
||||
probes: { ...metadata.serverDiagnostics.probes },
|
||||
mcpServers: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function metadataResourceSucceeded(
|
||||
metadata: SharedServerMetadata,
|
||||
method: keyof SharedServerMetadata["serverDiagnostics"]["probes"],
|
||||
): boolean {
|
||||
if (method === "model/list") return metadata.availableModels.length > 0 && metadata.serverDiagnostics.probes[method].status === "ok";
|
||||
return metadata.serverDiagnostics.probes[method].status === "ok";
|
||||
}
|
||||
|
||||
function mergeServerDiagnostics(previous: SharedServerMetadata, next: SharedServerMetadata): SharedServerMetadata["serverDiagnostics"] {
|
||||
const probes = { ...next.serverDiagnostics.probes };
|
||||
for (const method of Object.keys(probes) as (keyof typeof probes)[]) {
|
||||
if (probes[method].status === "failed") probes[method] = previous.serverDiagnostics.probes[method];
|
||||
}
|
||||
return {
|
||||
probes,
|
||||
mcpServers:
|
||||
next.serverDiagnostics.probes["mcpServerStatus/list"].status === "failed"
|
||||
? previous.serverDiagnostics.mcpServers.map((server) => ({ ...server }))
|
||||
: next.serverDiagnostics.mcpServers.map((server) => ({ ...server })),
|
||||
};
|
||||
}
|
||||
|
||||
function cloneSharedThreadListSnapshot(snapshot: SharedThreadListSnapshot): SharedThreadListSnapshot {
|
||||
return { status: snapshot.status, threads: cloneThreads(snapshot.threads) };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ export class SharedAppServerCache {
|
|||
}
|
||||
|
||||
applyAppServerMetadataSnapshot(context: SharedAppServerCacheContext, metadata: SharedServerMetadata): void {
|
||||
if (!isCacheableSharedServerMetadata(metadata)) return;
|
||||
this.state = applySharedServerMetadata(this.state, context, metadata);
|
||||
}
|
||||
|
||||
|
|
@ -85,12 +84,3 @@ export class SharedAppServerCache {
|
|||
return cachedSharedModels(this.state, context);
|
||||
}
|
||||
}
|
||||
|
||||
function isCacheableSharedServerMetadata(metadata: SharedServerMetadata): boolean {
|
||||
return (
|
||||
metadata.availableModels.length > 0 &&
|
||||
metadata.serverDiagnostics.probes["model/list"].status === "ok" &&
|
||||
metadata.serverDiagnostics.probes["skills/list"].status === "ok" &&
|
||||
metadata.serverDiagnostics.probes["account/rateLimits/read"].status === "ok"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createServerDiagnostics } from "../../src/domain/server/diagnostics";
|
||||
import { createServerDiagnostics, diagnosticProbeError, diagnosticProbeOk } from "../../src/domain/server/diagnostics";
|
||||
import type { RateLimitSnapshot } from "../../src/app-server/protocol/runtime-metrics";
|
||||
import { emptyRuntimeConfigSnapshot } from "../../src/app-server/protocol/runtime-config";
|
||||
import {
|
||||
|
|
@ -45,7 +45,7 @@ describe("shared app-server cache state", () => {
|
|||
availableSkills: [{ name: "skill", description: "", path: "/tmp/skill", enabled: true }],
|
||||
rateLimit: sourceRateLimit,
|
||||
serverDiagnostics: {
|
||||
...createServerDiagnostics(),
|
||||
...diagnostics({}),
|
||||
mcpServers: [{ name: "server", startupStatus: "ready", authStatus: null, toolCount: 1, message: null }],
|
||||
},
|
||||
});
|
||||
|
|
@ -103,6 +103,50 @@ describe("shared app-server cache state", () => {
|
|||
expect(cachedSharedServerMetadata(state, incompleteContext)).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("merges app-server metadata by successful resource", () => {
|
||||
const context = cacheContext();
|
||||
const first = applySharedServerMetadata(createSharedAppServerState(), context, {
|
||||
runtimeConfig: emptyRuntimeConfigSnapshot(),
|
||||
availableModels: [modelFixture("gpt-5.5")],
|
||||
availableSkills: [{ name: "writer", description: "", path: "/tmp/writer", enabled: true }],
|
||||
rateLimit: rateLimitFixture(),
|
||||
serverDiagnostics: diagnostics({}),
|
||||
});
|
||||
|
||||
const second = applySharedServerMetadata(first, context, {
|
||||
runtimeConfig: emptyRuntimeConfigSnapshot(),
|
||||
availableModels: [modelFixture("gpt-5.6")],
|
||||
availableSkills: [{ name: "stale", description: "", path: "/tmp/stale", enabled: true }],
|
||||
rateLimit: null,
|
||||
serverDiagnostics: diagnostics({ skills: "failed", rateLimit: "failed" }),
|
||||
});
|
||||
|
||||
const cached = expectPresent(cachedSharedServerMetadata(second, context));
|
||||
expect(cached.availableModels.map((model) => model.model)).toEqual(["gpt-5.6"]);
|
||||
expect(cached.availableSkills.map((skill) => skill.name)).toEqual(["writer"]);
|
||||
expect(cached.rateLimit?.primary?.usedPercent).toBe(42);
|
||||
expect(expectPresent(cachedSharedModels(second, context)).map((model) => model.model)).toEqual(["gpt-5.6"]);
|
||||
});
|
||||
|
||||
it("loads initial app-server metadata while caching only successful resources", () => {
|
||||
const context = cacheContext();
|
||||
const state = applySharedServerMetadata(createSharedAppServerState(), context, {
|
||||
runtimeConfig: emptyRuntimeConfigSnapshot(),
|
||||
availableModels: [modelFixture("failed-model")],
|
||||
availableSkills: [{ name: "writer", description: "", path: "/tmp/writer", enabled: true }],
|
||||
rateLimit: rateLimitFixture(),
|
||||
serverDiagnostics: diagnostics({ models: "failed", rateLimit: "failed" }),
|
||||
});
|
||||
|
||||
const cached = expectPresent(cachedSharedServerMetadata(state, context));
|
||||
expect(cached.runtimeConfig).not.toBeNull();
|
||||
expect(cached.serverDiagnostics.probes["model/list"].status).toBe("failed");
|
||||
expect(cached.availableModels).toEqual([]);
|
||||
expect(cached.availableSkills.map((skill) => skill.name)).toEqual(["writer"]);
|
||||
expect(cached.rateLimit).toBeNull();
|
||||
expect(cachedSharedModels(state, context)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
function cacheContext(overrides: Partial<SharedAppServerCacheContext> = {}): SharedAppServerCacheContext {
|
||||
|
|
@ -157,3 +201,16 @@ function rateLimitFixture(): RateLimitSnapshot {
|
|||
rateLimitReachedType: null,
|
||||
};
|
||||
}
|
||||
|
||||
function diagnostics(overrides: { models?: "ok" | "failed"; skills?: "ok" | "failed"; rateLimit?: "ok" | "failed" }) {
|
||||
const next = createServerDiagnostics();
|
||||
next.probes["model/list"] =
|
||||
overrides.models === "failed" ? diagnosticProbeError("model/list", new Error("offline")) : diagnosticProbeOk("model/list");
|
||||
next.probes["skills/list"] =
|
||||
overrides.skills === "failed" ? diagnosticProbeError("skills/list", new Error("offline")) : diagnosticProbeOk("skills/list");
|
||||
next.probes["account/rateLimits/read"] =
|
||||
overrides.rateLimit === "failed"
|
||||
? diagnosticProbeError("account/rateLimits/read", new Error("offline"))
|
||||
: diagnosticProbeOk("account/rateLimits/read");
|
||||
return next;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,30 +3,63 @@ import { describe, expect, it, vi } from "vitest";
|
|||
import { createServerDiagnostics, diagnosticProbeError, diagnosticProbeOk } from "../../src/domain/server/diagnostics";
|
||||
import { SharedAppServerCache } from "../../src/app-server/services/shared-cache";
|
||||
import { emptyRuntimeConfigSnapshot, type RuntimeConfigSnapshot } from "../../src/app-server/protocol/runtime-config";
|
||||
import type { RateLimitSnapshot } from "../../src/app-server/protocol/runtime-metrics";
|
||||
import type { SharedAppServerCacheContext, SharedServerMetadata } from "../../src/app-server/services/shared-cache-state";
|
||||
import type { ModelMetadata } from "../../src/domain/catalog/metadata";
|
||||
import type { ModelMetadata, SkillMetadata } from "../../src/domain/catalog/metadata";
|
||||
|
||||
describe("SharedAppServerCache", () => {
|
||||
it("does not store failed or empty metadata snapshots as shared cache truth", () => {
|
||||
it("updates successful metadata resources while preserving failed resource cache values", () => {
|
||||
const cache = new SharedAppServerCache();
|
||||
const context = cacheContext();
|
||||
const goodMetadata = metadata();
|
||||
const goodMetadata = metadata({
|
||||
availableSkills: [skillMetadata("writer")],
|
||||
rateLimit: rateLimit(42),
|
||||
});
|
||||
|
||||
cache.applyAppServerMetadataSnapshot(context, goodMetadata);
|
||||
expect(cache.cachedAppServerMetadata(context)?.availableModels.map((model) => model.model)).toEqual(["gpt-5.5"]);
|
||||
|
||||
const invalidSnapshots = [
|
||||
metadata({ availableModels: [] }),
|
||||
metadata({ modelProbeStatus: "failed" }),
|
||||
metadata({ skillsProbeStatus: "failed" }),
|
||||
metadata({ rateLimitProbeStatus: "failed" }),
|
||||
];
|
||||
cache.applyAppServerMetadataSnapshot(
|
||||
context,
|
||||
metadata({
|
||||
availableModels: [modelMetadata("gpt-5.6")],
|
||||
availableSkills: [skillMetadata("stale-skill")],
|
||||
rateLimit: rateLimit(90),
|
||||
skillsProbeStatus: "failed",
|
||||
rateLimitProbeStatus: "failed",
|
||||
}),
|
||||
);
|
||||
|
||||
for (const snapshot of invalidSnapshots) {
|
||||
cache.applyAppServerMetadataSnapshot(context, snapshot);
|
||||
}
|
||||
expect(cache.cachedAppServerMetadata(context)?.availableModels.map((model) => model.model)).toEqual(["gpt-5.6"]);
|
||||
expect(cache.cachedAppServerMetadata(context)?.availableSkills.map((skill) => skill.name)).toEqual(["writer"]);
|
||||
expect(cache.cachedAppServerMetadata(context)?.rateLimit?.primary?.usedPercent).toBe(42);
|
||||
|
||||
expect(cache.cachedAppServerMetadata(context)?.availableModels.map((model) => model.model)).toEqual(["gpt-5.5"]);
|
||||
cache.applyAppServerMetadataSnapshot(context, metadata({ availableModels: [], modelProbeStatus: "failed" }));
|
||||
expect(cache.cachedAppServerMetadata(context)?.availableModels.map((model) => model.model)).toEqual(["gpt-5.6"]);
|
||||
});
|
||||
|
||||
it("loads initial metadata snapshots without caching failed resource values", () => {
|
||||
const cache = new SharedAppServerCache();
|
||||
const context = cacheContext();
|
||||
|
||||
cache.applyAppServerMetadataSnapshot(
|
||||
context,
|
||||
metadata({
|
||||
availableModels: [modelMetadata("failed-model")],
|
||||
availableSkills: [skillMetadata("writer")],
|
||||
rateLimit: rateLimit(90),
|
||||
modelProbeStatus: "failed",
|
||||
rateLimitProbeStatus: "failed",
|
||||
}),
|
||||
);
|
||||
|
||||
const cached = cache.cachedAppServerMetadata(context);
|
||||
expect(cached?.runtimeConfig).not.toBeNull();
|
||||
expect(cached?.serverDiagnostics.probes["model/list"].status).toBe("failed");
|
||||
expect(cached?.availableModels).toEqual([]);
|
||||
expect(cached?.availableSkills.map((skill) => skill.name)).toEqual(["writer"]);
|
||||
expect(cached?.rateLimit).toBeNull();
|
||||
expect(cache.cachedModels(context)).toBeNull();
|
||||
});
|
||||
|
||||
it("does not share or store snapshots before the app-server identity is known", async () => {
|
||||
|
|
@ -148,6 +181,8 @@ function cacheContext(overrides: Partial<SharedAppServerCacheContext> = {}): Sha
|
|||
function metadata(
|
||||
overrides: {
|
||||
availableModels?: readonly ModelMetadata[];
|
||||
availableSkills?: readonly SkillMetadata[];
|
||||
rateLimit?: RateLimitSnapshot | null;
|
||||
runtimeConfig?: RuntimeConfigSnapshot | null;
|
||||
modelProbeStatus?: "ok" | "failed";
|
||||
skillsProbeStatus?: "ok" | "failed";
|
||||
|
|
@ -170,12 +205,27 @@ function metadata(
|
|||
return {
|
||||
runtimeConfig: overrides.runtimeConfig ?? emptyRuntimeConfigSnapshot(),
|
||||
availableModels: overrides.availableModels ?? [modelMetadata("gpt-5.5")],
|
||||
availableSkills: [],
|
||||
rateLimit: null,
|
||||
availableSkills: overrides.availableSkills ?? [],
|
||||
rateLimit: overrides.rateLimit ?? null,
|
||||
serverDiagnostics: diagnostics,
|
||||
};
|
||||
}
|
||||
|
||||
function skillMetadata(name: string): SkillMetadata {
|
||||
return { name, description: "", path: `/tmp/${name}`, enabled: true };
|
||||
}
|
||||
|
||||
function rateLimit(usedPercent: number): RateLimitSnapshot {
|
||||
return {
|
||||
limitId: "codex",
|
||||
limitName: "Codex",
|
||||
primary: { usedPercent, windowDurationMins: 300, resetsAt: 1 },
|
||||
secondary: null,
|
||||
individualLimit: null,
|
||||
rateLimitReachedType: null,
|
||||
};
|
||||
}
|
||||
|
||||
function modelMetadata(model: string): ModelMetadata {
|
||||
return {
|
||||
id: model,
|
||||
|
|
|
|||
Loading…
Reference in a new issue