Detach shared app-server cache snapshots

This commit is contained in:
murashit 2026-05-28 22:15:15 +09:00
parent 2040883f70
commit ec13c7daa1
2 changed files with 123 additions and 7 deletions

View file

@ -32,34 +32,62 @@ export function createSharedAppServerState(): SharedAppServerState {
export function applySharedThreadList(state: SharedAppServerState, threads: readonly Thread[]): SharedAppServerState {
return {
...state,
threads: { kind: "loaded", data: threads },
threads: { kind: "loaded", data: cloneThreads(threads) },
};
}
export function applySharedAppServerMetadata(state: SharedAppServerState, metadata: SharedAppServerMetadata): SharedAppServerState {
const clonedMetadata = cloneSharedAppServerMetadata(metadata);
return {
...state,
appServerMetadata: { kind: "loaded", data: metadata },
availableModels: metadata.availableModels,
appServerMetadata: { kind: "loaded", data: clonedMetadata },
availableModels: cloneModels(clonedMetadata.availableModels),
};
}
export function applySharedModels(state: SharedAppServerState, models: readonly Model[]): SharedAppServerState {
const clonedModels = cloneModels(models);
return {
...state,
appServerMetadata:
state.appServerMetadata.kind === "loaded"
? { kind: "loaded", data: { ...state.appServerMetadata.data, availableModels: models } }
? { kind: "loaded", data: { ...state.appServerMetadata.data, availableModels: cloneModels(clonedModels) } }
: state.appServerMetadata,
availableModels: models,
availableModels: clonedModels,
};
}
export function cachedSharedThreadList(state: SharedAppServerState): readonly Thread[] | null {
return state.threads.kind === "loaded" ? state.threads.data : null;
return state.threads.kind === "loaded" ? cloneThreads(state.threads.data) : null;
}
export function cachedSharedAppServerMetadata(state: SharedAppServerState): SharedAppServerMetadata | null {
if (state.appServerMetadata.kind === "loaded") return state.appServerMetadata.data;
if (state.appServerMetadata.kind === "loaded") return cloneSharedAppServerMetadata(state.appServerMetadata.data);
return null;
}
function cloneSharedAppServerMetadata(metadata: SharedAppServerMetadata): SharedAppServerMetadata {
return {
...metadata,
availableModels: cloneModels(metadata.availableModels),
availableSkills: metadata.availableSkills.map((skill) => ({ ...skill })),
appServerDiagnostics: {
probes: { ...metadata.appServerDiagnostics.probes },
mcpServers: metadata.appServerDiagnostics.mcpServers.map((server) => ({ ...server })),
},
};
}
function cloneThreads(threads: readonly Thread[]): Thread[] {
return threads.map((thread) => ({ ...thread, turns: [...thread.turns] }));
}
function cloneModels(models: readonly Model[]): Model[] {
return models.map((model) => ({
...model,
supportedReasoningEfforts: [...model.supportedReasoningEfforts],
inputModalities: [...model.inputModalities],
additionalSpeedTiers: [...model.additionalSpeedTiers],
serviceTiers: [...model.serviceTiers],
}));
}

View file

@ -1,6 +1,9 @@
import { describe, expect, it } from "vitest";
import type { ConfigReadResponse } from "../../src/generated/app-server/v2/ConfigReadResponse";
import type { Model } from "../../src/generated/app-server/v2/Model";
import type { Thread } from "../../src/generated/app-server/v2/Thread";
import { createAppServerDiagnostics } from "../../src/app-server/compatibility";
import {
compactContextLabel,
compactModelLabel,
@ -27,6 +30,14 @@ import {
} from "../../src/runtime/state";
import { readRuntimeConfig } from "../../src/runtime/config";
import { contextSummary, effectiveConfigSections, rateLimitSummary } from "../../src/runtime/view";
import {
applySharedAppServerMetadata,
applySharedModels,
applySharedThreadList,
cachedSharedAppServerMetadata,
cachedSharedThreadList,
createSharedAppServerState,
} from "../../src/runtime/shared-app-server-state";
describe("runtime settings", () => {
it("parses model overrides", () => {
@ -35,6 +46,38 @@ describe("runtime settings", () => {
expect(parseModelOverride("")).toBeUndefined();
});
it("keeps shared app-server cache snapshots detached from caller-owned arrays", () => {
const sourceThreads = [threadFixture("thread-1")];
const threadState = applySharedThreadList(createSharedAppServerState(), sourceThreads);
sourceThreads.push(threadFixture("thread-2"));
const cachedThreads = cachedSharedThreadList(threadState);
expect(cachedThreads?.map((thread) => thread.id)).toEqual(["thread-1"]);
const mutableCachedThreads = cachedThreads as Thread[];
mutableCachedThreads.push(threadFixture("thread-3"));
expect(cachedSharedThreadList(threadState)?.map((thread) => thread.id)).toEqual(["thread-1"]);
const sourceModels = [modelFixture("gpt-5.5")];
const modelState = applySharedModels(createSharedAppServerState(), sourceModels);
sourceModels.push(modelFixture("gpt-5.6"));
expect(modelState.availableModels.map((model) => model.model)).toEqual(["gpt-5.5"]);
const metadataState = applySharedAppServerMetadata(createSharedAppServerState(), {
effectiveConfig: null,
availableModels: sourceModels,
availableSkills: [{ name: "skill", description: "", path: "/tmp/skill", scope: "repo", enabled: true }],
rateLimit: null,
appServerDiagnostics: {
...createAppServerDiagnostics(),
mcpServers: [{ name: "server", startupStatus: "ready", authStatus: null, toolCount: 1, message: null }],
},
});
sourceModels.push(modelFixture("gpt-5.7"));
const cachedMetadata = cachedSharedAppServerMetadata(metadataState);
expect(cachedMetadata?.availableModels.map((model) => model.model)).toEqual(["gpt-5.5", "gpt-5.6"]);
});
it("parses reasoning effort overrides", () => {
expect(parseReasoningEffortOverride("high")).toBe("high");
expect(parseReasoningEffortOverride("default")).toBeNull();
@ -591,3 +634,48 @@ function configLayer(config: Record<string, unknown>, profile: string | null): N
disabledReason: null,
};
}
function threadFixture(id: string): Thread {
return {
id,
sessionId: "session",
forkedFromId: null,
preview: "",
ephemeral: false,
modelProvider: "openai",
createdAt: 1,
updatedAt: 1,
status: { type: "idle" },
path: null,
cwd: "/vault",
cliVersion: "0.0.0",
source: "appServer",
threadSource: null,
agentNickname: null,
agentRole: null,
gitInfo: null,
name: null,
turns: [],
};
}
function modelFixture(model: string): Model {
return {
id: model,
model,
upgrade: null,
upgradeInfo: null,
availabilityNux: null,
displayName: model,
description: "",
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: "medium",
inputModalities: [],
supportsPersonality: false,
additionalSpeedTiers: [],
serviceTiers: [],
defaultServiceTier: null,
isDefault: false,
};
}