refactor: keep runtime context in memory

This commit is contained in:
2026-06-07 17:07:14 +08:00
parent 1ed7e56f35
commit 9d4e5486e9
11 changed files with 273 additions and 269 deletions
+6 -6
View File
@@ -2,7 +2,10 @@ import { randomUUID } from "node:crypto";
import { logger } from "../logger.js";
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
import { SessionRuntimeContextStore } from "../sessions/runtimeContextStore.js";
import {
removeRuntimeSessionContext,
setRuntimeSessionContext,
} from "../runtime/sessionContext.js";
import { toActorKey, toProjectKey } from "../utils/fileStore.js";
export type SessionBinding = {
@@ -26,7 +29,6 @@ export type ChatRequestContext = SessionContext & {
export class ChatSessionBridge {
private readonly abortControllers = new Map<string, AbortController>();
private readonly sessionRuntimeContextStore = new SessionRuntimeContextStore();
constructor(private readonly runtime: OpencodeRuntimeAdapter) {}
@@ -61,7 +63,7 @@ export class ChatSessionBridge {
sessionId,
startedAt: Date.now(),
};
await this.sessionRuntimeContextStore.write({
setRuntimeSessionContext({
accessToken: requestContext.accessToken,
actorKey: requestContext.actorKey,
allowLearningWrite: true,
@@ -133,9 +135,7 @@ export class ChatSessionBridge {
"failed while waiting for runtime session to become idle",
);
});
await this.sessionRuntimeContextStore.remove(sessionId).catch((error) => {
logger.debug({ sessionId, err: error }, "failed to cleanup runtime tool context");
});
removeRuntimeSessionContext(sessionId);
}
private buildRequestContext(context: {
-2
View File
@@ -47,8 +47,6 @@ const envSchema = z
OPENCODE_SKILLS_ROOT_DIR: z.string().default("./.opencode/skills"),
// client 模式下,目标 opencode server 的基础地址。
OPENCODE_CLIENT_BASE_URL: z.string().url().optional(),
// 提供给本地 opencode tools 读取的会话上下文目录。
SESSION_RUNTIME_CONTEXT_STORAGE_DIR: z.string().default("./data/session-runtime-contexts"),
// tjwater-cli 可执行文件路径。
TJWATER_CLI_PATH: z.string().default("./cli/tjwater-cli"),
// TJWater 后端 API 的基础地址。
+9 -10
View File
@@ -9,7 +9,10 @@ import { SessionLearningStateStore } from "./sessionStateStore.js";
import { MemoryStore, type MemoryScope } from "../memory/store.js";
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
import { SkillStore } from "../skills/store.js";
import { SessionRuntimeContextStore } from "../sessions/runtimeContextStore.js";
import {
removeRuntimeSessionContext,
setRuntimeSessionContext,
} from "../runtime/sessionContext.js";
import {
sanitizePersistentDocument,
sanitizePersistentLine,
@@ -76,7 +79,6 @@ export class LearningOrchestrator {
private readonly activeReviews = new Set<string>();
private readonly sessionLearningStateStore = new SessionLearningStateStore();
private readonly skillStore = new SkillStore();
private readonly sessionRuntimeContextStore = new SessionRuntimeContextStore();
constructor(
private readonly runtime: OpencodeRuntimeAdapter,
@@ -85,10 +87,7 @@ export class LearningOrchestrator {
) {}
async initialize() {
await Promise.all([
this.sessionLearningStateStore.initialize(),
this.sessionRuntimeContextStore.initialize(),
]);
await this.sessionLearningStateStore.initialize();
}
async onTurnCompleted(input: TurnReviewInput) {
@@ -147,7 +146,7 @@ export class LearningOrchestrator {
`learning-gate-${input.requestContext.clientSessionId}`,
);
gateSessionId = gateSession.id;
await this.sessionRuntimeContextStore.write({
setRuntimeSessionContext({
actorKey: input.requestContext.actorKey,
allowLearningWrite: false,
clientSessionId: `gate-${input.requestContext.clientSessionId}`,
@@ -215,7 +214,7 @@ export class LearningOrchestrator {
});
} finally {
if (gateSessionId) {
await this.sessionRuntimeContextStore.remove(gateSessionId).catch(() => undefined);
removeRuntimeSessionContext(gateSessionId);
await this.runtime.abortSession(gateSessionId).catch(() => undefined);
}
}
@@ -235,7 +234,7 @@ export class LearningOrchestrator {
const reviewSession = await this.runtime.createSession(
`learning-review-${input.requestContext.clientSessionId}`,
);
await this.sessionRuntimeContextStore.write({
setRuntimeSessionContext({
actorKey: input.requestContext.actorKey,
allowLearningWrite: false,
clientSessionId: `review-${input.requestContext.clientSessionId}`,
@@ -283,7 +282,7 @@ export class LearningOrchestrator {
traceId: input.requestContext.traceId,
});
} finally {
await this.sessionRuntimeContextStore.remove(reviewSession.id).catch(() => undefined);
removeRuntimeSessionContext(reviewSession.id);
await this.runtime.abortSession(reviewSession.id).catch(() => undefined);
}
}
+27
View File
@@ -0,0 +1,27 @@
export type RuntimeSessionContext = {
accessToken?: string;
actorKey: string;
allowLearningWrite?: boolean;
clientSessionId: string;
learningMode?: "interactive" | "review";
memoryListReadScopes?: Partial<Record<"user" | "workspace", boolean>>;
projectId?: string;
projectKey: string;
sessionId: string;
traceId: string;
};
const contexts = new Map<string, RuntimeSessionContext>();
export const setRuntimeSessionContext = (context: RuntimeSessionContext) => {
contexts.set(context.sessionId, { ...context });
};
export const getRuntimeSessionContext = (sessionId: string) => {
const context = contexts.get(sessionId);
return context ? { ...context } : null;
};
export const removeRuntimeSessionContext = (sessionId: string) => {
contexts.delete(sessionId);
};
+4 -6
View File
@@ -18,7 +18,7 @@ import {
} from "./results/store.js";
import { buildChatRouter } from "./routes/chat.js";
import { opencodeRuntime } from "./runtime/opencode.js";
import { SessionRuntimeContextStore } from "./sessions/runtimeContextStore.js";
import { getRuntimeSessionContext } from "./runtime/sessionContext.js";
const app = express();
@@ -28,7 +28,6 @@ const sessionMetadataStore = new SessionMetadataStore();
const sessionUiStateStore = new SessionUiStateStore();
const memoryStore = new MemoryStore();
const sessionTranscriptStore = new SessionTranscriptStore();
const sessionRuntimeContextStore = new SessionRuntimeContextStore();
const learningOrchestrator = new LearningOrchestrator(
opencodeRuntime,
memoryStore,
@@ -71,7 +70,7 @@ app.post("/internal/tools/tjwater-cli-call", async (req, res) => {
const sessionId =
typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
const context = sessionId ? await sessionRuntimeContextStore.read(sessionId) : null;
const context = sessionId ? getRuntimeSessionContext(sessionId) : null;
if (!context) {
res.status(404).json({
message: "session context not found",
@@ -175,7 +174,7 @@ app.post("/internal/tools/store-render-ref", async (req, res) => {
const sessionId =
typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
const filePath = typeof req.body?.file_path === "string" ? req.body.file_path.trim() : "";
const context = sessionId ? await sessionRuntimeContextStore.read(sessionId) : null;
const context = sessionId ? getRuntimeSessionContext(sessionId) : null;
if (!context) {
res.status(404).json({
message: "session context not found",
@@ -225,7 +224,7 @@ app.post("/internal/tools/session-search", async (req, res) => {
const sessionId =
typeof req.body?.session_id === "string" ? req.body.session_id.trim() : "";
const query = typeof req.body?.query === "string" ? req.body.query : "";
const context = sessionId ? await sessionRuntimeContextStore.read(sessionId) : null;
const context = sessionId ? getRuntimeSessionContext(sessionId) : null;
if (!context) {
res.status(404).json({
message: "session context not found",
@@ -273,7 +272,6 @@ const bootstrap = async () => {
memoryStore.initialize(),
resultReferenceStore.initialize(),
sessionTranscriptStore.initialize(),
sessionRuntimeContextStore.initialize(),
]);
resultReferenceStore.startCleanupLoop();
};
-46
View File
@@ -1,46 +0,0 @@
import { join } from "node:path";
import { config } from "../config.js";
import {
atomicWriteJson,
ensureDirectory,
readJsonFile,
removeFileIfExists,
} from "../utils/fileStore.js";
export type SessionRuntimeContext = {
accessToken?: string;
actorKey: string;
allowLearningWrite?: boolean;
clientSessionId: string;
learningMode?: "interactive" | "review";
memoryListReadScopes?: Partial<Record<"user" | "workspace", boolean>>;
projectId?: string;
projectKey: string;
sessionId: string;
traceId: string;
};
export class SessionRuntimeContextStore {
constructor(private readonly baseDir = config.SESSION_RUNTIME_CONTEXT_STORAGE_DIR) {}
async initialize() {
await ensureDirectory(this.baseDir);
}
async write(context: SessionRuntimeContext) {
await atomicWriteJson(this.filePath(context.sessionId), context);
}
async read(sessionId: string) {
return await readJsonFile<SessionRuntimeContext>(this.filePath(sessionId));
}
async remove(sessionId: string) {
await removeFileIfExists(this.filePath(sessionId));
}
private filePath(sessionId: string) {
return join(this.baseDir, `${sessionId}.json`);
}
}