Bound shared query and catalog context retention

This commit is contained in:
murashit 2026-07-10 12:44:46 +09:00
parent c0e4fc590c
commit b6f6d04147
4 changed files with 39 additions and 2 deletions

View file

@ -32,6 +32,7 @@ import { cloneModelMetadata, cloneSharedServerMetadata, cloneThreads } from "./s
const THREAD_LIST_STALE_TIME_MS = 10_000;
const APP_SERVER_METADATA_STALE_TIME_MS = 10_000;
const MODELS_STALE_TIME_MS = 60_000;
const APP_SERVER_QUERY_GC_TIME_MS = 5 * 60_000;
export interface AppServerQueryClientRunner {
runWithClient<T>(
@ -388,7 +389,7 @@ function createAppServerQueryClient(): QueryClient {
return new QueryClient({
defaultOptions: {
queries: {
gcTime: Infinity,
gcTime: APP_SERVER_QUERY_GC_TIME_MS,
retry: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,

View file

@ -2,6 +2,7 @@ import type { ObservedResultListener } from "../../../app-server/query/observed-
import type { Thread } from "../../../domain/threads/model";
type ThreadListObserver = ObservedResultListener<readonly Thread[]>;
const MAX_RETAINED_THREAD_CATALOG_CONTEXTS = 4;
interface ThreadCatalogStore {
contextKey(): string;
@ -113,7 +114,20 @@ export function createThreadCatalog(options: ThreadCatalogOptions): ThreadCatalo
function threadCatalogFactsForContext(factsByContext: Map<string, ThreadCatalogFacts>, contextKey: string): ThreadCatalogFacts {
const existing = factsByContext.get(contextKey);
if (existing) return existing;
if (existing) {
factsByContext.delete(contextKey);
factsByContext.set(contextKey, existing);
return existing;
}
while (factsByContext.size >= MAX_RETAINED_THREAD_CATALOG_CONTEXTS) {
let leastRecentlyUsed: string | undefined;
for (const key of factsByContext.keys()) {
leastRecentlyUsed = key;
break;
}
if (leastRecentlyUsed === undefined) break;
factsByContext.delete(leastRecentlyUsed);
}
const facts = {
active: pendingThreadListFacts(),
archived: pendingThreadListFacts(),

View file

@ -17,6 +17,12 @@ import {
import type { SharedServerMetadata } from "../../src/domain/server/metadata";
describe("AppServerQueryCache", () => {
it("allows inactive query contexts to be garbage-collected", () => {
const cache = new AppServerQueryCache();
expect(cache.client.getDefaultOptions().queries?.gcTime).toBe(300_000);
});
it("preserves successful metadata resource values when probes fail", () => {
const cache = new AppServerQueryCache();
const context = cacheContext();

View file

@ -194,6 +194,22 @@ describe("ThreadCatalog", () => {
expect(catalog.activeSnapshot()).toEqual([thread("started-a")]);
});
it("evicts lifecycle facts for least-recently-used connection contexts", () => {
const context = { codexPath: "codex-a", vaultPath: "/vault" };
const { catalog } = catalogFixture({ context: () => context });
catalog.apply({ type: "thread-started", thread: thread("started-a") });
for (const suffix of ["b", "c", "d", "e"]) {
context.codexPath = `codex-${suffix}`;
catalog.activeSnapshot();
}
context.codexPath = "codex-a";
catalog.apply({ type: "active-list-snapshot-received", threads: [thread("server-a")] });
expect(catalog.activeSnapshot()).toEqual([thread("server-a")]);
});
it("keeps rollback fork metadata until active snapshots catch up to the rollback version", () => {
const { catalog } = catalogFixture();
const forkBeforeRollback = thread("forked", false, { name: "Before", preview: "Before rollback", updatedAt: 20 });