feat(v0.1): add mgr session PVC lifecycle for true session state persistence
PR B for #770: mgr/session-pvc.ts + server endpoints + selftest. - 新模块 src/mgr/session-pvc.ts: createSessionPvc / getSessionPvcSummary / deleteSessionPvc / refreshSessionPvcSummary / runSessionStorageGc / startSessionStorageGcLoop - Server 增量 4 个 endpoint: * POST /api/v1/sessions: 创建 session 同步创建 PVC * GET /api/v1/sessions/:id/storage: 查询 PVC 摘要 * DELETE /api/v1/sessions/:id/storage: 删 PVC + storage_kind=evicted * POST /api/v1/sessions/:id/storage/refresh: runner 上报 PVC 摘要 * POST /api/v1/sessions/storage/gc: 手动触发 GC - mgr SA RBAC 已在 PR A 增加;manager server 不直连 Kubernetes API(kubectl 由 mgr 容器内执行) - SessionRecord 增量 storageKind / storagePvcName / storageNamespace / storageSizeBytes / storageFilesCount / storageSha256 / storageUpdatedAt / storagePvcPhase / storageEvictedAt / codexRolloutSubdir - kubernetes-runner-job 短路:run 引用 evicted session 时直接返回 session-store-evicted,不创建 runner Job - KubectlHandler 可注入,selftest 覆盖 create / summary / refresh / eviction / gc / REST 路径 - GC loop 默认 5min(AGENTRUN_SESSION_GC_INTERVAL_MS 可调) runner / backend / HWLAB adapter 在 PR C / PR D 落地。
This commit is contained in:
+107
-1
@@ -1,4 +1,4 @@
|
||||
import type { BackendProfile, BackendTurnResult, CommandRecord, CreateCommandInput, CreateQueueTaskInput, CreateRunInput, FailureKind, JsonRecord, QueueAttemptRef, QueueCommanderSnapshot, QueueReadCursorRecord, QueueStats, QueueTaskListResult, QueueTaskRecord, QueueTaskState, RunEvent, RunnerJobRecord, RunnerRecord, RunRecord, SessionEventPage, SessionListResult, SessionListState, SessionReadCursorRecord, SessionRecord, SessionRef, SessionSummary, TerminalStatus } from "../common/types.js";
|
||||
import type { BackendProfile, BackendTurnResult, CommandRecord, CreateCommandInput, CreateQueueTaskInput, CreateRunInput, FailureKind, JsonRecord, ListGcExpiredSessionsInput, QueueAttemptRef, QueueCommanderSnapshot, QueueReadCursorRecord, QueueStats, QueueTaskListResult, QueueTaskRecord, QueueTaskState, RunEvent, RunnerJobRecord, RunnerRecord, RunRecord, SessionEventPage, SessionListResult, SessionListState, SessionReadCursorRecord, SessionRecord, SessionRef, SessionStoragePatch, SessionSummary, TerminalStatus, UpsertSessionInput } from "../common/types.js";
|
||||
import { AgentRunError } from "../common/errors.js";
|
||||
import { newId, nowIso, stableHash } from "../common/validation.js";
|
||||
import { redactJson } from "../common/redaction.js";
|
||||
@@ -44,6 +44,10 @@ export interface AgentRunStore {
|
||||
listSessionTrace(sessionId: string, input: SessionEventPageInput): MaybePromise<SessionEventPage>;
|
||||
listSessionOutput(sessionId: string, input: SessionEventPageInput): MaybePromise<SessionEventPage>;
|
||||
markSessionRead(sessionId: string, readerId: string): MaybePromise<SessionReadCursorRecord>;
|
||||
upsertSession(input: UpsertSessionInput): MaybePromise<SessionRecord>;
|
||||
refreshSessionStorage(input: SessionStoragePatch): MaybePromise<SessionRecord>;
|
||||
markSessionStorageEvicted(input: { sessionId: string; pvcName: string }): MaybePromise<SessionRecord>;
|
||||
listGcExpiredSessions(input: ListGcExpiredSessionsInput): MaybePromise<SessionRecord[]>;
|
||||
createQueueTask(input: CreateQueueTaskInput): MaybePromise<QueueTaskRecord>;
|
||||
listQueueTasks(input: ListQueueTasksInput): MaybePromise<QueueTaskListResult>;
|
||||
getQueueTask(taskId: string): MaybePromise<QueueTaskRecord>;
|
||||
@@ -344,6 +348,108 @@ export class MemoryAgentRunStore implements AgentRunStore {
|
||||
return record;
|
||||
}
|
||||
|
||||
upsertSession(input: UpsertSessionInput): SessionRecord {
|
||||
const at = nowIso();
|
||||
const existing = this.sessions.get(input.sessionId);
|
||||
if (existing) {
|
||||
const next: SessionRecord = {
|
||||
...existing,
|
||||
tenantId: input.tenantId,
|
||||
projectId: input.projectId,
|
||||
backendProfile: input.backendProfile,
|
||||
conversationId: input.conversationId,
|
||||
threadId: input.threadId,
|
||||
metadata: input.metadata,
|
||||
expiresAt: input.expiresAt,
|
||||
codexRolloutSubdir: input.codexRolloutSubdir,
|
||||
version: existing.version + 1,
|
||||
updatedAt: at,
|
||||
};
|
||||
this.sessions.set(input.sessionId, next);
|
||||
this.sessionVersion = Math.max(this.sessionVersion, next.version) + 1;
|
||||
return next;
|
||||
}
|
||||
const next: SessionRecord = {
|
||||
sessionId: input.sessionId,
|
||||
tenantId: input.tenantId,
|
||||
projectId: input.projectId,
|
||||
backendProfile: input.backendProfile,
|
||||
conversationId: input.conversationId,
|
||||
threadId: input.threadId,
|
||||
metadata: input.metadata,
|
||||
version: 1,
|
||||
executionState: "idle",
|
||||
lastRunId: null,
|
||||
lastCommandId: null,
|
||||
activeRunId: null,
|
||||
activeCommandId: null,
|
||||
lastEventSeq: 0,
|
||||
terminalStatus: null,
|
||||
failureKind: null,
|
||||
title: null,
|
||||
summary: {},
|
||||
lastActivityAt: null,
|
||||
createdAt: at,
|
||||
updatedAt: at,
|
||||
expiresAt: input.expiresAt,
|
||||
storageKind: "none",
|
||||
codexRolloutSubdir: input.codexRolloutSubdir,
|
||||
};
|
||||
this.sessions.set(input.sessionId, next);
|
||||
this.sessionVersion = Math.max(this.sessionVersion, next.version) + 1;
|
||||
return next;
|
||||
}
|
||||
|
||||
refreshSessionStorage(input: SessionStoragePatch): SessionRecord {
|
||||
const session = this.sessions.get(input.sessionId);
|
||||
if (!session) throw new AgentRunError("schema-invalid", `session ${input.sessionId} was not found`, { httpStatus: 404 });
|
||||
const at = nowIso();
|
||||
const next: SessionRecord = {
|
||||
...session,
|
||||
storageKind: input.storageKind,
|
||||
storagePvcName: input.pvcName ?? null,
|
||||
storageNamespace: input.storageNamespace ?? null,
|
||||
storagePvcPhase: input.pvcPhase ?? null,
|
||||
storageSizeBytes: input.storageSizeBytes ?? null,
|
||||
storageFilesCount: input.storageFilesCount ?? null,
|
||||
storageSha256: input.storageSha256 ?? null,
|
||||
codexRolloutSubdir: input.codexRolloutSubdir ?? session.codexRolloutSubdir ?? "sessions",
|
||||
storageUpdatedAt: at,
|
||||
version: session.version + 1,
|
||||
updatedAt: at,
|
||||
};
|
||||
this.sessions.set(input.sessionId, next);
|
||||
this.sessionVersion = Math.max(this.sessionVersion, next.version) + 1;
|
||||
return next;
|
||||
}
|
||||
|
||||
markSessionStorageEvicted(input: { sessionId: string; pvcName: string }): SessionRecord {
|
||||
const session = this.sessions.get(input.sessionId);
|
||||
if (!session) throw new AgentRunError("schema-invalid", `session ${input.sessionId} was not found`, { httpStatus: 404 });
|
||||
const at = nowIso();
|
||||
const next: SessionRecord = {
|
||||
...session,
|
||||
storageKind: "evicted",
|
||||
storagePvcName: input.pvcName,
|
||||
storageEvictedAt: at,
|
||||
storageUpdatedAt: at,
|
||||
version: session.version + 1,
|
||||
updatedAt: at,
|
||||
};
|
||||
this.sessions.set(input.sessionId, next);
|
||||
this.sessionVersion = Math.max(this.sessionVersion, next.version) + 1;
|
||||
return next;
|
||||
}
|
||||
|
||||
listGcExpiredSessions(input: ListGcExpiredSessionsInput): SessionRecord[] {
|
||||
const now = input.now;
|
||||
return Array.from(this.sessions.values())
|
||||
.filter((session) => session.storageKind === "pvc" && Boolean(session.storagePvcName))
|
||||
.filter((session) => session.expiresAt !== null && Date.parse(session.expiresAt ?? "") <= now)
|
||||
.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt))
|
||||
.slice(0, Math.max(1, input.limit));
|
||||
}
|
||||
|
||||
createQueueTask(input: CreateQueueTaskInput): QueueTaskRecord {
|
||||
const payloadHash = stableHash(input.payload);
|
||||
if (input.idempotencyKey) {
|
||||
|
||||
Reference in New Issue
Block a user