Use model queries as the sole model metadata source

This commit is contained in:
murashit 2026-07-10 12:37:05 +09:00
parent dccd63b75b
commit c375f110a2
9 changed files with 52 additions and 120 deletions

View file

@ -42,6 +42,8 @@ Obsidian and app-server boundaries stay outside Preact components. External life
Chat-visible state belongs in the chat state store and named reducer actions. Signals and components may project that state, but they should not become parallel sources of truth.
Shared app-server resources should likewise have one query record per resource. In particular, model catalog data belongs to the model query; broader server metadata may report the model probe status but must not carry a second model snapshot.
Preact Signals are a chat panel rendering adapter, not a second state system. Panel surfaces may read narrow signal-backed read models, but application workflows, domain code, presentation helpers, and pure UI components must not depend on broad reducer slices or reactive state primitives.
Imperative DOM bridges are allowed when an external API, host lifecycle, hit-test, focus/selection operation, or measurement problem requires an `HTMLElement`. They should remain narrow boundary adapters, not a second UI composition system inside Preact-owned surfaces.

View file

@ -194,11 +194,8 @@ export class AppServerQueryCache {
writeAppServerMetadata(context: AppServerQueryContext, metadata: SharedServerMetadata): SharedServerMetadata | null {
if (!appServerQueryContextIsComplete(context)) return null;
const next = metadataWithLastKnownGood(metadata, this.appServerMetadataSnapshot(context), this.modelsSnapshot(context));
const next = metadataWithLastKnownGood(metadata, this.appServerMetadataSnapshot(context));
this.client.setQueryData(appServerMetadataQueryKey(context), cloneSharedServerMetadata(next));
if (metadata.serverDiagnostics.probes.models.status === "ok") {
this.client.setQueryData(appServerModelsQueryKey(context), cloneModelMetadata(next.availableModels));
}
return cloneSharedServerMetadata(next);
}
@ -273,27 +270,25 @@ export class AppServerQueryCache {
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([
const [modelProbe, skills, permissionProfiles, rateLimit] = await Promise.all([
this.readModelMetadataProbe(refreshContext, client),
readSkillMetadataProbe(client, refreshContext.vaultPath, options.forceSkills ?? false),
readPermissionProfileMetadataProbe(client, refreshContext.vaultPath),
readRateLimitMetadataProbe(client),
]);
const diagnostics = [models.probe, skills.probe, permissionProfiles.probe, rateLimit.probe].reduce(
const diagnostics = [modelProbe, skills.probe, permissionProfiles.probe, rateLimit.probe].reduce(
(current, probe) => diagnosticsWithProbe(current, probe),
previous?.serverDiagnostics ?? createServerDiagnostics(),
);
return metadataWithLastKnownGood(
{
runtimeConfig,
availableModels: models.value,
availableSkills: skills.value,
availablePermissionProfiles: permissionProfiles.value,
rateLimit: rateLimit.value,
serverDiagnostics: diagnostics,
},
previous,
this.modelsSnapshot(refreshContext),
);
});
},
@ -319,18 +314,12 @@ export class AppServerQueryCache {
private async readModelMetadataProbe(
context: AppServerQueryContext,
client: AppServerClient,
): Promise<{
value: readonly ModelMetadata[];
probe: SharedServerMetadata["serverDiagnostics"]["probes"]["models"];
}> {
): Promise<SharedServerMetadata["serverDiagnostics"]["probes"]["models"]> {
try {
const models = cloneModelMetadata(await this.client.fetchQuery(this.modelsQueryOptionsWithClient(context, client)));
return { value: models, probe: diagnosticProbeOk("models", `${String(models.length)} models`, Date.now()) };
return diagnosticProbeOk("models", `${String(models.length)} models`, Date.now());
} catch (error) {
return {
value: this.modelsSnapshot(context) ?? [],
probe: diagnosticProbeError("models", error, Date.now()),
};
return diagnosticProbeError("models", error, Date.now());
}
}
@ -383,15 +372,10 @@ export class AppServerQueryCache {
}
}
function metadataWithLastKnownGood(
metadata: SharedServerMetadata,
previous: SharedServerMetadata | null,
models: readonly ModelMetadata[] | null,
): SharedServerMetadata {
function metadataWithLastKnownGood(metadata: SharedServerMetadata, previous: SharedServerMetadata | 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 ?? []),

View file

@ -25,7 +25,6 @@ export function cloneSharedServerMetadata(metadata: SharedServerMetadata): Share
...metadata,
runtimeConfig: metadata.runtimeConfig ? cloneRuntimeConfigSnapshot(metadata.runtimeConfig) : null,
rateLimit: metadata.rateLimit ? cloneRateLimitSnapshot(metadata.rateLimit) : null,
availableModels: cloneModelMetadata(metadata.availableModels),
availableSkills: metadata.availableSkills.map((skill) => ({ ...skill })),
availablePermissionProfiles: metadata.availablePermissionProfiles.map((profile) => ({ ...profile })),
serverDiagnostics: {

View file

@ -1,4 +1,4 @@
import type { ModelMetadata, SkillMetadata } from "../catalog/metadata";
import type { SkillMetadata } from "../catalog/metadata";
import type { RuntimeConfigSnapshot } from "../runtime/config";
import type { RateLimitSnapshot } from "../runtime/metrics";
import type { RuntimePermissionProfileSummary } from "../runtime/permissions";
@ -6,7 +6,6 @@ import type { Diagnostics } from "./diagnostics";
export interface SharedServerMetadata {
runtimeConfig: RuntimeConfigSnapshot | null;
availableModels: readonly ModelMetadata[];
availableSkills: readonly SkillMetadata[];
availablePermissionProfiles: readonly RuntimePermissionProfileSummary[];
rateLimit: RateLimitSnapshot | null;

View file

@ -66,7 +66,6 @@ function applyAppServerMetadata(host: ServerMetadataActionsHost, metadata: Share
host.stateStore.dispatch({
type: "connection/metadata-applied",
runtimeConfig: metadata.runtimeConfig,
availableModels: metadata.availableModels,
availableSkills: metadata.availableSkills,
availablePermissionProfiles: metadata.availablePermissionProfiles,
rateLimit: metadata.rateLimit,

View file

@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest";
import type { CatalogModel, CatalogSkillMetadata } from "../../src/app-server/protocol/catalog";
import { AppServerQueryCache } from "../../src/app-server/query/cache";
import type { AppServerQueryContext } from "../../src/app-server/query/keys";
import type { ModelMetadata, SkillMetadata } from "../../src/domain/catalog/metadata";
import type { SkillMetadata } from "../../src/domain/catalog/metadata";
import { emptyRuntimeConfigSnapshot, type RuntimeConfigSnapshot } from "../../src/domain/runtime/config";
import type { RateLimitSnapshot } from "../../src/domain/runtime/metrics";
import type { RuntimePermissionProfileSummary } from "../../src/domain/runtime/permissions";
@ -17,7 +17,7 @@ import {
import type { SharedServerMetadata } from "../../src/domain/server/metadata";
describe("AppServerQueryCache", () => {
it("preserves successful resource values when metadata probes fail", () => {
it("preserves successful metadata resource values when probes fail", () => {
const cache = new AppServerQueryCache();
const context = cacheContext();
const goodMetadata = metadata({
@ -27,12 +27,9 @@ describe("AppServerQueryCache", () => {
});
cache.writeAppServerMetadata(context, goodMetadata);
expect(cache.appServerMetadataSnapshot(context)?.availableModels.map((model) => model.model)).toEqual(["gpt-5.5"]);
cache.writeAppServerMetadata(
context,
metadata({
availableModels: [modelMetadata("gpt-5.6")],
availableSkills: [skillMetadata("failed-skill")],
availablePermissionProfiles: [permissionProfile(":failed")],
rateLimit: rateLimit(90),
@ -42,15 +39,13 @@ describe("AppServerQueryCache", () => {
}),
);
expect(cache.appServerMetadataSnapshot(context)?.availableModels.map((model) => model.model)).toEqual(["gpt-5.6"]);
expect(cache.appServerMetadataSnapshot(context)?.availableSkills.map((skill) => skill.name)).toEqual(["writer"]);
expect(cache.appServerMetadataSnapshot(context)?.availablePermissionProfiles.map((profile) => profile.id)).toEqual([":workspace"]);
expect(cache.appServerMetadataSnapshot(context)?.rateLimit?.primary?.usedPercent).toBe(42);
expect(cache.appServerMetadataSnapshot(context)?.serverDiagnostics.probes.skills.status).toBe("failed");
expect(cache.appServerMetadataSnapshot(context)?.serverDiagnostics.probes.rateLimits.status).toBe("failed");
cache.writeAppServerMetadata(context, metadata({ availableModels: [], modelProbeStatus: "failed" }));
expect(cache.appServerMetadataSnapshot(context)?.availableModels.map((model) => model.model)).toEqual(["gpt-5.6"]);
cache.writeAppServerMetadata(context, metadata({ modelProbeStatus: "failed" }));
expect(cache.appServerMetadataSnapshot(context)?.serverDiagnostics.probes.models.status).toBe("failed");
});
@ -61,7 +56,6 @@ describe("AppServerQueryCache", () => {
cache.writeAppServerMetadata(
context,
metadata({
availableModels: [modelMetadata("gpt-previous")],
availableSkills: [skillMetadata("previous-skill")],
availablePermissionProfiles: [permissionProfile(":previous")],
rateLimit: rateLimit(11),
@ -71,7 +65,6 @@ describe("AppServerQueryCache", () => {
cache.writeAppServerMetadata(
context,
metadata({
availableModels: [modelMetadata("gpt-next")],
availableSkills: [skillMetadata("next-skill")],
availablePermissionProfiles: [permissionProfile(":next")],
rateLimit: rateLimit(22),
@ -84,14 +77,6 @@ describe("AppServerQueryCache", () => {
const snapshot = cache.appServerMetadataSnapshot(context);
const statusCase = metadataProbeStatusCase(statuses);
expect(
snapshot?.availableModels.map((model) => model.model),
statusCase,
).toEqual([statuses.models === "ok" ? "gpt-next" : "gpt-previous"]);
expect(
cache.modelsSnapshot(context)?.map((model) => model.model),
statusCase,
).toEqual([statuses.models === "ok" ? "gpt-next" : "gpt-previous"]);
expect(
snapshot?.availableSkills.map((skill) => skill.name),
statusCase,
@ -111,7 +96,6 @@ describe("AppServerQueryCache", () => {
cache.writeAppServerMetadata(
context,
metadata({
availableModels: [modelMetadata("failed-model")],
availableSkills: [skillMetadata("writer")],
availablePermissionProfiles: [permissionProfile(":workspace")],
rateLimit: rateLimit(90),
@ -125,7 +109,6 @@ describe("AppServerQueryCache", () => {
const cached = cache.appServerMetadataSnapshot(context);
expect(cached?.runtimeConfig).not.toBeNull();
expect(cached?.serverDiagnostics.probes.models.status).toBe("failed");
expect(cached?.availableModels).toEqual([]);
expect(cached?.availableSkills).toEqual([]);
expect(cached?.availablePermissionProfiles).toEqual([]);
expect(cached?.rateLimit).toBeNull();
@ -186,15 +169,16 @@ describe("AppServerQueryCache", () => {
expect(cache.modelsSnapshot(context)).toBeNull();
});
it("stores successful empty model snapshots as shared cache truth", () => {
const cache = new AppServerQueryCache();
it("does not let metadata writes overwrite the model query", async () => {
const cache = cacheWithRequestHandlers({
"model/list": vi.fn().mockResolvedValue({ data: [catalogModel("gpt-model-query")] }),
});
const context = cacheContext();
cache.writeAppServerMetadata(context, metadata({ availableModels: [modelMetadata("gpt-5.6")] }));
cache.writeAppServerMetadata(context, metadata({ availableModels: [] }));
await cache.fetchModels(context);
cache.writeAppServerMetadata(context, metadata({ modelProbeStatus: "failed" }));
expect(cache.appServerMetadataSnapshot(context)?.availableModels).toEqual([]);
expect(cache.modelsSnapshot(context)).toEqual([]);
expect(cache.modelsSnapshot(context)?.map((model) => model.model)).toEqual(["gpt-model-query"]);
});
it("does not reuse metadata or model snapshots across app-server cache contexts", () => {
@ -279,7 +263,7 @@ describe("AppServerQueryCache", () => {
expect(cache.activeThreadsSnapshot(context)).toBeNull();
});
it("fetches app-server metadata through the query cache and syncs model snapshots", async () => {
it("fetches app-server metadata and models through their respective query records", async () => {
const context = cacheContext();
const cache = cacheWithRequestHandlers({
"config/read": vi.fn().mockResolvedValue({}),
@ -291,7 +275,6 @@ describe("AppServerQueryCache", () => {
const metadata = await cache.refreshAppServerMetadata(context);
expect(metadata?.availableModels.map((model) => model.model)).toEqual(["gpt-meta"]);
expect(metadata?.availableSkills.map((skill) => skill.name)).toEqual(["writer"]);
expect(metadata?.availablePermissionProfiles.map((profile) => profile.id)).toEqual([":workspace"]);
expect(metadata?.rateLimit?.primary?.usedPercent).toBe(64);
@ -321,36 +304,41 @@ describe("AppServerQueryCache", () => {
modelRefresh.resolve({ data: [catalogModel("gpt-shared")] });
await expect(modelsPromise).resolves.toMatchObject([{ model: "gpt-shared" }]);
await expect(metadataPromise).resolves.toMatchObject({
availableModels: [{ model: "gpt-shared" }],
});
await expect(metadataPromise).resolves.toMatchObject({ serverDiagnostics: { probes: { models: { status: "ok" } } } });
expect(listModels).toHaveBeenCalledOnce();
expect(cache.modelsSnapshot(context)?.map((model) => model.model)).toEqual(["gpt-shared"]);
});
it("keeps query-cached models when app-server metadata model refresh fails", async () => {
const context = cacheContext();
const listModels = vi
.fn()
.mockResolvedValueOnce({ data: [catalogModel("gpt-cached")] })
.mockRejectedValueOnce(new Error("offline"));
const cache = cacheWithRequestHandlers({
"config/read": vi.fn().mockResolvedValue({}),
"model/list": vi.fn().mockRejectedValue(new Error("offline")),
"model/list": listModels,
"skills/list": vi.fn().mockResolvedValue({ data: [{ skills: [] }] }),
"permissionProfile/list": vi.fn().mockResolvedValue({ data: [], nextCursor: null }),
"account/rateLimits/read": vi.fn().mockResolvedValue({ rateLimits: appServerRateLimit(0), rateLimitsByLimitId: null }),
});
cache.writeAppServerMetadata(context, metadata({ availableModels: [modelMetadata("gpt-cached")] }));
await cache.fetchModels(context);
const metadataSnapshot = await cache.refreshAppServerMetadata(context);
expect(metadataSnapshot?.availableModels.map((model) => model.model)).toEqual(["gpt-cached"]);
expect(metadataSnapshot?.serverDiagnostics.probes.models.status).toBe("failed");
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 listModels = vi
.fn()
.mockResolvedValueOnce({ data: [catalogModel("gpt-cached")] })
.mockRejectedValueOnce(new Error("models offline"));
const cache = cacheWithRequestHandlers({
"config/read": vi.fn().mockResolvedValue({}),
"model/list": vi.fn().mockRejectedValue(new Error("models offline")),
"model/list": listModels,
"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")),
@ -358,16 +346,16 @@ describe("AppServerQueryCache", () => {
cache.writeAppServerMetadata(
context,
metadata({
availableModels: [modelMetadata("gpt-cached")],
availableSkills: [skillMetadata("cached-skill")],
availablePermissionProfiles: [permissionProfile(":cached")],
rateLimit: rateLimit(17),
}),
);
await cache.fetchModels(context);
const refreshed = await cache.refreshAppServerMetadata(context);
expect(refreshed?.availableModels.map((model) => model.model)).toEqual(["gpt-cached"]);
expect(cache.modelsSnapshot(context)?.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);
@ -455,7 +443,6 @@ function cacheWithRequestHandlers(handlers: Record<string, (params: unknown) =>
function metadata(
overrides: {
availableModels?: readonly ModelMetadata[];
availableSkills?: readonly SkillMetadata[];
availablePermissionProfiles?: readonly RuntimePermissionProfileSummary[];
rateLimit?: RateLimitSnapshot | null;
@ -493,7 +480,6 @@ function metadata(
);
return {
runtimeConfig: overrides.runtimeConfig ?? emptyRuntimeConfigSnapshot(),
availableModels: overrides.availableModels ?? [modelMetadata("gpt-5.5")],
availableSkills: overrides.availableSkills ?? [],
availablePermissionProfiles: overrides.availablePermissionProfiles ?? [],
rateLimit: overrides.rateLimit ?? null,
@ -542,23 +528,6 @@ function rateLimit(usedPercent: number): RateLimitSnapshot {
};
}
function modelMetadata(model: string): ModelMetadata {
return {
id: model,
model,
displayName: model,
description: "",
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: "medium",
inputModalities: [],
additionalSpeedTiers: [],
serviceTiers: [],
defaultServiceTier: null,
isDefault: false,
};
}
function catalogModel(model: string): CatalogModel {
return {
id: model,

View file

@ -39,7 +39,7 @@ describe("AppServerSharedQueries", () => {
const refresh = queries.refreshAppServerMetadata();
context.vaultPath = "/other-vault";
pending.resolve(serverMetadata({ availableModels: [model("stale-model")] }));
pending.resolve(serverMetadata());
await expect(refresh).rejects.toBeInstanceOf(StaleAppServerSharedQueryContextError);
});
@ -106,14 +106,15 @@ describe("AppServerSharedQueries", () => {
});
it("publishes metadata and model snapshots to shared query observers", () => {
const metadata = serverMetadata({ availableModels: [model("gpt-test")] });
const metadata = serverMetadata();
const models = [model("gpt-test")];
const metadataListener = vi.fn();
const modelListener = vi.fn();
const queries = new AppServerSharedQueries({
cache: cacheWith({
appServerMetadataSnapshot: () => metadata,
updateAppServerMetadata: () => metadata,
modelsSnapshot: () => metadata.availableModels,
modelsSnapshot: () => models,
observeAppServerMetadataResult: (_context, listener) => {
metadataObserver = listener;
return () => undefined;
@ -132,12 +133,12 @@ describe("AppServerSharedQueries", () => {
queries.observeModelsResult(modelListener);
queries.updateAppServerMetadata(() => metadata);
metadataObserver(observedResult(metadata));
modelObserver(observedResult(metadata.availableModels));
modelObserver(observedResult(models));
expect(queries.appServerMetadataSnapshot()).toEqual(metadata);
expect(queries.modelsSnapshot()).toEqual(metadata.availableModels);
expect(queries.modelsSnapshot()).toEqual(models);
expect(metadataListener).toHaveBeenLastCalledWith(expect.objectContaining({ value: metadata }));
expect(modelListener).toHaveBeenCalledWith(expect.objectContaining({ value: metadata.availableModels }));
expect(modelListener).toHaveBeenCalledWith(expect.objectContaining({ value: models }));
});
});
@ -197,7 +198,6 @@ function serverMetadata(overrides: Partial<SharedServerMetadata> = {}): SharedSe
const diagnostics = diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("models", "0 models", 1));
return {
runtimeConfig: null,
availableModels: [],
availableSkills: [],
availablePermissionProfiles: [],
rateLimit: null,

View file

@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import type { ModelMetadata, SkillMetadata } from "../../../../../src/domain/catalog/metadata";
import type { SkillMetadata } from "../../../../../src/domain/catalog/metadata";
import { emptyRuntimeConfigSnapshot } from "../../../../../src/domain/runtime/config";
import type { RateLimitSnapshot } from "../../../../../src/domain/runtime/metrics";
import {
@ -24,9 +24,9 @@ import { deferred } from "../../../../support/async";
import { chatStateFixture, chatStateWith } from "../../support/state";
describe("server metadata actions", () => {
it("applies refreshed app-server metadata", async () => {
it("applies refreshed non-model app-server metadata", async () => {
const stateStore = createChatStateStore(chatStateFixture());
const metadata = serverMetadataFixture({ availableModels: [modelFixture("gpt-5.1")] });
const metadata = serverMetadataFixture({ availableSkills: [skillFixture("writer")] });
const actions = createServerMetadataActions({
stateStore,
metadataResourceTransport: metadataResourceTransport(),
@ -37,7 +37,7 @@ describe("server metadata actions", () => {
await actions.refreshAppServerMetadata();
expect(stateStore.getState().connection.availableModels.map((model) => model.model)).toEqual(["gpt-5.1"]);
expect(stateStore.getState().connection.availableSkills.map((skill) => skill.name)).toEqual(["writer"]);
});
it("preserves panel-local tool diagnostics when applying shared metadata", async () => {
@ -208,7 +208,6 @@ describe("server diagnostics actions", () => {
it("reuses refreshed app-server metadata for deferred diagnostics", async () => {
const stateStore = createChatStateStore(chatStateFixture());
const refreshedMetadata = serverMetadataFixture({
availableModels: [modelFixture("gpt-5.1")],
availableSkills: [skillFixture("writer")],
serverDiagnostics: diagnosticsWithProbe(
diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("models", "1 models", 1)),
@ -458,23 +457,6 @@ function toolInventory(): ToolInventorySnapshot {
};
}
function modelFixture(model: string): ModelMetadata {
return {
id: model,
model,
displayName: model,
description: "",
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: "medium",
inputModalities: ["text"],
additionalSpeedTiers: [],
serviceTiers: [],
defaultServiceTier: null,
isDefault: false,
};
}
function skillFixture(name: string): SkillMetadata {
return {
name,
@ -499,7 +481,6 @@ function rateLimitFixture(overrides: Partial<RateLimitSnapshot> = {}): RateLimit
function serverMetadataFixture(overrides: Partial<SharedServerMetadata> = {}): SharedServerMetadata {
return {
runtimeConfig: emptyRuntimeConfigSnapshot(),
availableModels: [],
availableSkills: [],
availablePermissionProfiles: [],
rateLimit: null,

View file

@ -456,7 +456,6 @@ describe("CodexChatView connection lifecycle", () => {
() =>
({
runtimeConfig: { ...emptyRuntimeConfigSnapshot(), model: "gpt-cached" },
availableModels: [],
availableSkills: [{ name: "writer", enabled: true }],
availablePermissionProfiles: [],
rateLimit: null,
@ -1026,7 +1025,7 @@ interface ChatHostFixtureOverrides {
function chatHost(overrides: ChatHostFixtureOverrides = {}): TestCodexChatHost {
let activeThreads = overrides.activeSnapshot?.() ?? null;
let metadata = overrides.appServerMetadataSnapshot?.() ?? null;
const models = overrides.modelsSnapshot?.() ?? null;
let models = overrides.modelsSnapshot?.() ?? null;
const activeThreadResultListeners = new Set<(result: ObservedResult<readonly Thread[]>) => void>();
const metadataResultListeners = new Set<(result: ObservedResult<SharedServerMetadata>) => void>();
const modelResultListeners = new Set<(result: ObservedResult<readonly ModelMetadata[]>) => void>();
@ -1040,7 +1039,6 @@ function chatHost(overrides: ChatHostFixtureOverrides = {}): TestCodexChatHost {
const applyMetadataToCache = (nextMetadata: SharedServerMetadata): SharedServerMetadata => {
metadata = nextMetadata;
for (const listener of metadataResultListeners) listener(queryResult(nextMetadata));
for (const listener of modelResultListeners) listener(queryResult(nextMetadata.availableModels));
return nextMetadata;
};
const loadAppServerMetadata = async (): Promise<SharedServerMetadata | null> => {
@ -1049,16 +1047,18 @@ function chatHost(overrides: ChatHostFixtureOverrides = {}): TestCodexChatHost {
const connectionStillCurrent = () => connectionMock.state.client === client && connectionMock.state.connected;
await client.request("config/read", { cwd: vaultPath, includeLayers: true });
if (!connectionStillCurrent()) return null;
let availableModels: readonly ModelMetadata[];
let fetchedModels: readonly ModelMetadata[];
if (overrides.fetchModels) {
availableModels = await overrides.fetchModels();
fetchedModels = await overrides.fetchModels();
} else {
const modelsResponse = (await client.request("model/list", { includeHidden: false, limit: 100 })) as {
data: Parameters<typeof modelMetadataFromCatalogModels>[0];
};
availableModels = modelMetadataFromCatalogModels(modelsResponse.data);
fetchedModels = modelMetadataFromCatalogModels(modelsResponse.data);
}
if (!connectionStillCurrent()) return null;
models = fetchedModels;
for (const listener of modelResultListeners) listener(queryResult(fetchedModels));
const skillsResponse = (await client.request("skills/list", { cwds: [vaultPath], forceReload: false })) as {
data: { skills: { name: string; description?: string; path?: string; enabled?: boolean }[] }[];
};
@ -1072,7 +1072,6 @@ function chatHost(overrides: ChatHostFixtureOverrides = {}): TestCodexChatHost {
if (!connectionStillCurrent()) return null;
return {
runtimeConfig: emptyRuntimeConfigSnapshot(),
availableModels,
availableSkills: skillsResponse.data.flatMap(
(entry: { skills: { name: string; description?: string; path?: string; enabled?: boolean }[] }) =>
entry.skills.map((skill) => ({