refactor: unify agent session persistence

This commit is contained in:
2026-06-04 15:02:27 +08:00
parent 04ded0ceb0
commit 0ecb2babf3
22 changed files with 542 additions and 497 deletions
+19 -22
View File
@@ -3,13 +3,13 @@ import { z } from "zod";
import { writeLearningAuditLog } from "../audit/learningAudit.js";
import { type ChatRequestContext } from "../chat/sessionBridge.js";
import { config } from "../config.js";
import { type SessionTurnRecord, SessionHistoryStore } from "../history/store.js";
import { type SessionTurnRecord, SessionTranscriptStore } from "../sessions/transcriptStore.js";
import { logger } from "../logger.js";
import { LearningStateStore } from "./stateStore.js";
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 { ToolSessionContextStore } from "../session/toolContextStore.js";
import { SessionRuntimeContextStore } from "../sessions/runtimeContextStore.js";
import {
sanitizePersistentDocument,
sanitizePersistentLine,
@@ -68,25 +68,25 @@ type TurnReviewInput = {
export class LearningOrchestrator {
private readonly activeReviews = new Set<string>();
private readonly learningStateStore = new LearningStateStore();
private readonly sessionLearningStateStore = new SessionLearningStateStore();
private readonly skillStore = new SkillStore();
private readonly toolContextStore = new ToolSessionContextStore();
private readonly sessionRuntimeContextStore = new SessionRuntimeContextStore();
constructor(
private readonly runtime: OpencodeRuntimeAdapter,
private readonly memoryStore: MemoryStore,
private readonly historyStore: SessionHistoryStore,
private readonly transcriptStore: SessionTranscriptStore,
) {}
async initialize() {
await Promise.all([
this.learningStateStore.initialize(),
this.toolContextStore.initialize(),
this.sessionLearningStateStore.initialize(),
this.sessionRuntimeContextStore.initialize(),
]);
}
async onTurnCompleted(input: TurnReviewInput) {
const transcript = await this.historyStore.appendTurn(
const transcript = await this.transcriptStore.appendTurn(
{
actorKey: input.requestContext.actorKey,
clientSessionId: input.requestContext.clientSessionId,
@@ -105,13 +105,12 @@ export class LearningOrchestrator {
}
this.activeReviews.add(input.sessionId);
try {
const state = await this.learningStateStore.read(input.sessionId);
const state = await this.sessionLearningStateStore.read(input.sessionId);
const turnsSinceGate = Math.max(0, turnCount - state.lastGatedTurn);
if (turnsSinceGate < config.LEARNING_GATE_TURN_COOLDOWN || state.pendingReview) {
if (turnsSinceGate < config.LEARNING_GATE_TURN_COOLDOWN) {
this.activeReviews.delete(input.sessionId);
return;
}
await this.learningStateStore.markPending(input.sessionId, true);
} catch (error) {
this.activeReviews.delete(input.sessionId);
throw error;
@@ -142,7 +141,7 @@ export class LearningOrchestrator {
`learning-gate-${input.requestContext.clientSessionId}`,
);
gateSessionId = gateSession.id;
await this.toolContextStore.write({
await this.sessionRuntimeContextStore.write({
actorKey: input.requestContext.actorKey,
allowLearningWrite: false,
clientSessionId: `gate-${input.requestContext.clientSessionId}`,
@@ -164,7 +163,7 @@ export class LearningOrchestrator {
const gateText = collectTextContent(assistantMessage?.parts ?? []);
const gate = parseGateResult(gateText);
if (!gate) {
await this.learningStateStore.completeGate(input.sessionId, turnCount);
await this.sessionLearningStateStore.completeGate(input.sessionId, turnCount);
await writeLearningAuditLog({
action: "review-gate",
detail: "gate result was not valid JSON",
@@ -189,7 +188,7 @@ export class LearningOrchestrator {
traceId: input.requestContext.traceId,
});
if (!shouldPromote) {
await this.learningStateStore.completeGate(input.sessionId, turnCount);
await this.sessionLearningStateStore.completeGate(input.sessionId, turnCount);
return;
}
await this.runReview({
@@ -199,7 +198,6 @@ export class LearningOrchestrator {
turnCount,
});
} catch (error) {
await this.learningStateStore.markPending(input.sessionId, false);
logger.warn({ err: error, sessionId: input.sessionId }, "learning gate failed");
await writeLearningAuditLog({
action: "review-gate",
@@ -211,7 +209,7 @@ export class LearningOrchestrator {
});
} finally {
if (gateSessionId) {
await this.toolContextStore.remove(gateSessionId).catch(() => undefined);
await this.sessionRuntimeContextStore.remove(gateSessionId).catch(() => undefined);
await this.runtime.abortSession(gateSessionId).catch(() => undefined);
}
}
@@ -231,7 +229,7 @@ export class LearningOrchestrator {
const reviewSession = await this.runtime.createSession(
`learning-review-${input.requestContext.clientSessionId}`,
);
await this.toolContextStore.write({
await this.sessionRuntimeContextStore.write({
actorKey: input.requestContext.actorKey,
allowLearningWrite: false,
clientSessionId: `review-${input.requestContext.clientSessionId}`,
@@ -254,7 +252,7 @@ export class LearningOrchestrator {
const reviewText = collectTextContent(assistantMessage?.parts ?? []);
const parsed = parseReviewResult(reviewText);
if (!parsed) {
await this.learningStateStore.completeGate(input.sessionId, turnCount);
await this.sessionLearningStateStore.completeGate(input.sessionId, turnCount);
await writeLearningAuditLog({
action: "review-parse",
detail: "review result was not valid JSON",
@@ -266,9 +264,8 @@ export class LearningOrchestrator {
return;
}
await this.applyReviewResult(input, parsed, turnCount);
await this.learningStateStore.completeReview(input.sessionId, turnCount);
await this.sessionLearningStateStore.completeReview(input.sessionId, turnCount);
} catch (error) {
await this.learningStateStore.markPending(input.sessionId, false);
logger.warn({ err: error, sessionId: input.sessionId }, "learning review failed");
await writeLearningAuditLog({
action: "review-run",
@@ -279,7 +276,7 @@ export class LearningOrchestrator {
traceId: input.requestContext.traceId,
});
} finally {
await this.toolContextStore.remove(reviewSession.id).catch(() => undefined);
await this.sessionRuntimeContextStore.remove(reviewSession.id).catch(() => undefined);
await this.runtime.abortSession(reviewSession.id).catch(() => undefined);
}
}