From 87beb00bdbd3bb6df311fa7f872abee30a74f4b1 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 3 Jun 2026 18:45:13 +0800 Subject: [PATCH] feat(v0.1): add per-session RWO PVC foundation for true session state persistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR A for #770: docs + migration 007 + RBAC + types foundation. - 新增 failureKind session-store-evicted,用于区分 PVC 缺失与真协议错误 - 新增 migration 007_v01_session_state_storage:sessions 表增加 storage_* 列 + 索引 - mgr SA RBAC 增量:persistentvolumeclaims: [create, get, list, watch, delete] - 6 份 SPEC 升级(runtime-assembly / hwlab-manual-dispatch / backend-codex T7b / agentrun-runner / agentrun-mgr / services) - 显式禁止:fake app-server mock、replacement threadId、runner 启动后 copy/restore、idleTimeoutMs 拉永驻 - selftest 断言更新到 007_v01_session_state_storage 后续 PR B/C 在此基础上接入 mgr 端 PVC 生命周期 + runner 端 mount + backend 端 observability。 --- docs/reference/spec-v01-agentrun-mgr.md | 22 +++++++++++++ docs/reference/spec-v01-agentrun-runner.md | 32 +++++++++++++++++++ docs/reference/spec-v01-backend-codex.md | 11 +++++++ .../spec-v01-hwlab-manual-dispatch.md | 21 ++++++++++++ docs/reference/spec-v01-runtime-assembly.md | 22 +++++++++++++ docs/reference/spec-v01-services.md | 13 ++++++++ scripts/src/gitops-render.ts | 3 ++ src/common/types.ts | 1 + src/mgr/postgres-store.ts | 25 +++++++++++++++ src/selftest/cases/00-redaction-postgres.ts | 4 ++- 10 files changed, 153 insertions(+), 1 deletion(-) diff --git a/docs/reference/spec-v01-agentrun-mgr.md b/docs/reference/spec-v01-agentrun-mgr.md index 4645487..8ad1963 100644 --- a/docs/reference/spec-v01-agentrun-mgr.md +++ b/docs/reference/spec-v01-agentrun-mgr.md @@ -71,6 +71,28 @@ PATCH /api/v1/commands/:commandId/status 所有 API 成功和失败响应都必须是 JSON。失败响应至少包含 `failureKind`、`message` 和 trace correlation;不得出现空 stdout/空 response 被误判为成功的情况。 +### v0.1.1 Session state 存储 API + +在 `P1 SessionRef 持久化` 升级到「per-session RWO PVC 直接挂载」后,manager 必须提供下列受控 API 来管理 session 的 PVC 生命周期: + +```http +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` 同步创建 `agentrun-v01-session-` PVC(1Gi RWO,StorageClass 走 `AGENTRUN_SESSION_STORAGE_CLASS`),再返回 session record。 +- `GET /api/v1/sessions/:id/storage` 只返回 `pvcName` / `pvcPhase` / `storage_size_bytes` / `storage_files_count` / `storage_sha256` / `storage_updated_at` 摘要。 +- `DELETE /api/v1/sessions/:id/storage` 删 PVC 并回写 `storage_kind='evicted'` 与 `storage_evicted_at`。 +- `POST /api/v1/sessions/:id/storage/refresh` 写回 runner 报告的 PVC 摘要,写到 `agentrun_sessions` 表的 `storage_*` 列。 +- GC 循环(默认 5min,可调 `AGENTRUN_SESSION_GC_INTERVAL_MS`)扫到 `expires_at` 过 + 无 active run 的 session 删 PVC。 +- run 渲染前 `get pvc`:不存在且 `storage_kind='pvc'` 时短路返回 `session-store-evicted`,不创建 runner Job。 +- mgr SA 必须有 `persistentvolumeclaims: [create, get, list, watch, delete]` 权限(RBAC 由 deploy 模板提供)。 +- failureKind 矩阵新增 `session-store-evicted`,仅当 `AGENTRUN_SESSION_PVC_NAME` 已设 + codex 报 `no rollout found for thread id` 时使用;其他 `thread/resume` 失败按 T7 走 `thread-resume-failed`。 + ## HWLAB v0.2 基线承接 Manager 只承接 HWLAB v0.2 Code Agent 的通用执行事实,不承接 HWLAB 的用户鉴权、device-pod 授权或 Workbench schema。HWLAB 侧能力吸收总表见 [spec-v01-hwlab-manual-dispatch.md](spec-v01-hwlab-manual-dispatch.md)。本服务需要把以下能力固化为 AgentRun 自身合同: diff --git a/docs/reference/spec-v01-agentrun-runner.md b/docs/reference/spec-v01-agentrun-runner.md index 1a76ed1..4419b60 100644 --- a/docs/reference/spec-v01-agentrun-runner.md +++ b/docs/reference/spec-v01-agentrun-runner.md @@ -32,6 +32,38 @@ Kubernetes Job runner 必须把 credential source 与 runtime home 分开:Secr RuntimeAssembly P0 中 `SessionRef` 可以显式为 `null`,runner 不得把完整 `CODEX_HOME`、Secret projection 或节点 host path 当作 session store。`ResourceBundleRef` P0 收敛为 Git-only;runner 已支持把 `repoUrl + full commitId` checkout 到 `AGENTRUN_WORKSPACE_ROOT` 下的隔离目录,并记录 commit/tree 摘要,不能把用户上传文件或 env dump 混入 Git-only bundle。`toolAliases`、`promptRefs` 和 `skillRefs` 都属于该 Git-only bundle 的非敏感子资源:runner 必须在同一 checkout 中解析、校验和装配,不能从镜像默认目录、host path、旧 prompt 常量或用户长 prompt 中补齐。 +### v0.1.1 Session state 持久化(per-session RWO PVC 直接挂载) + +Runner 启动时若 run 携带 `storage_kind='pvc'` 的 session 引用,必须在 Job manifest 渲染阶段加一个 `agentrun-sessions` volume,把 `agentrun-v01-session-` PVC 直接挂到 `${CODEX_HOME}/`,并把以下 env 透传给 backend: + +- `AGENTRUN_SESSION_PVC_NAME`:PVC 名(`agentrun-v01-session-`)。 +- `AGENTRUN_SESSION_PVC_NAMESPACE`:默认 `agentrun-v01`。 +- `AGENTRUN_SESSION_PVC_MOUNT_PATH`:默认 `${CODEX_HOME}/`。 +- `AGENTRUN_CODEX_ROLLOUT_SUBDIR`:默认 `sessions`。 + +边界: + +- PVC 与 `runner-home` emptyDir 父目录共存;`auth.json` / `config.toml` / `state_*.sqlite` / `memories/` / `tmp/` / `skills/` 仍走 emptyDir;codex rollout JSONL 走 PVC。 +- `automountServiceAccountToken=false` 不变;runner 不通过 k8s API 读 PVC 内容,只把 PVC 摘要通过 `POST /api/v1/sessions/:id/storage/refresh` 写回 manager。 +- PVC 不存在 + `storage_kind='pvc'` 时 runner 启动前 manager 端短路返回 `session-store-evicted`;runner 不知道该 session。 +- 禁止 runner 启动后做 copy/restore(PR #78 已回退的 replacement 逻辑同源,禁止任何变体)。 + +Runner K8s manifest 增量(spec 形态参考): + +```yaml +volumes: + - name: runner-home # emptyDir(auth.json, config.toml, state_*.sqlite...) + emptyDir: {} + - name: agentrun-sessions # 新增 PVC RWO + persistentVolumeClaim: + claimName: agentrun-v01-session- +volumeMounts: + - name: runner-home + mountPath: /home/agentrun + - name: agentrun-sessions + mountPath: /home/agentrun/.codex-/ # <-- 唯一改这一行 +``` + ### ResourceBundle 子资源装配 Runner materialize `ResourceBundleRef` 后必须按固定顺序处理子资源:先创建 `toolAliases` wrapper,再装配 `skillRefs` registry,最后读取 `promptRefs` 并生成当前 command 的 assembled initial prompt 摘要。任何子资源缺失或非法都必须按对应 failureKind 阻塞当前 command,不得继续运行后让模型自行猜测。 diff --git a/docs/reference/spec-v01-backend-codex.md b/docs/reference/spec-v01-backend-codex.md index 636a6d2..58c6a83 100644 --- a/docs/reference/spec-v01-backend-codex.md +++ b/docs/reference/spec-v01-backend-codex.md @@ -136,6 +136,17 @@ Run 的 `executionPolicy.secretScope` 应引用与 `backendProfile` 匹配的 pr 阅读本文和 [spec-v01-runtime-assembly.md](spec-v01-runtime-assembly.md),然后构造一个带旧 `SessionRef.threadId` 的真实或 fake app-server run,使 `thread/resume` 返回 `no rollout found for thread id`。确认 adapter 输出 `thread-resume-failed` 并终止当前 turn;events/result/sessionRef 不得出现 `thread/resume:non-resumable`、替代 `thread/start`、新 threadId 回写或历史 prompt 拼接。确认 provider auth、rate limit、model config 或其他 protocol error 仍按各自 failureKind 直接失败,不走替代路径。 +### T7b Session state 持久化与 eviction + +阅读本文和 [spec-v01-runtime-assembly.md](spec-v01-runtime-assembly.md) v0.1.1 SessionRef 持久化节,然后构造一个带 `AGENTRUN_SESSION_PVC_NAME` 环境变量的真实或 fake app-server run。验证: + +1. `initialize` 之后 adapter emit `codex-rollout-storage-mounted` 事件,事件 payload 含 `pvcName` / `mountPath` / `codexRolloutSubdir`,`valuesPrinted=false`。 +2. codex 写 `${CODEX_HOME}//YYYY/MM/DD/rollout--.jsonl`,adapter 不写 PVC 内容,但 `runtimeSummary` 必须能回答 mount 是否成功。 +3. 删 PVC 后再次 turn:`AGENTRUN_SESSION_PVC_NAME` 已设 + `thread/resume` 返回 `no rollout found for thread id` 时,failureKind 升级为 `session-store-evicted`,**不是** `thread-resume-failed`。 +4. 同一 session 跨 runner pod 重建,PVC 不动:adapter 仍 emit `codex-rollout-storage-mounted`;`thread/resume:completed` 路径走通,events 不出现 `thread-resume-failed` / `thread/replacement-start:completed`。 +5. `AGENTRUN_SESSION_PVC_NAME` 未设 + `thread/resume` 失败:仍走 `thread-resume-failed`,不升级为 `session-store-evicted`。 +6. `codex_rollout_subdir` 走 env `AGENTRUN_CODEX_ROLLOUT_SUBDIR`,默认 `sessions`;codex CLI 改子目录时只改 env,不改装配。 + ### T8 Initial prompt and skill refs 阅读本文和 [spec-v01-runtime-assembly.md](spec-v01-runtime-assembly.md),然后用 fake app-server 或真实 Codex stdio run 验证 `ResourceBundleRef.promptRefs` 与 `skillRefs`。首轮无 threadId 时,`turn/start` input 必须包含 initial prompt 与 skill facts,并记录 `initialPromptInjected=true`;第二轮带同一 `threadId` resume 时,`turn/start` input 只能包含当前用户 message,记录 `initialPromptInjected=false`,且不得拼接第一轮 prompt、assistant 回复或旧 skill facts。required prompt/skill 缺失时不得调用 Codex provider。 diff --git a/docs/reference/spec-v01-hwlab-manual-dispatch.md b/docs/reference/spec-v01-hwlab-manual-dispatch.md index 707448d..e9ee249 100644 --- a/docs/reference/spec-v01-hwlab-manual-dispatch.md +++ b/docs/reference/spec-v01-hwlab-manual-dispatch.md @@ -121,6 +121,26 @@ AgentRun 需要提供 durable cancel 能力,建议形态为 `POST /api/v1/runs `SessionRef` 需要从 `null/deferred` 升级为可选持久会话引用,支持 HWLAB `conversationId/sessionId/threadId` 到 AgentRun session identity 的映射。session 只能保存 backend thread/session/cache,不保存 API KEY、`auth.json`、`config.toml` 或完整 `CODEX_HOME`。session store 必须与 Secret projection、writable runtime home、Git workspace 分离。runner 启动时,只按 command `payload.threadId` 或 `SessionRef.threadId` 执行 `thread/resume`,没有标准 `threadId` 则执行 `thread/start`;events、result 和 session record 都以 `threadId` 为唯一 thread identity;profile 隔离、TTL、GC 和跨 profile 污染防护必须可见。 +### v0.1.1 Session state 真正持久化(per-session RWO PVC 直接挂载) + +`P1 SessionRef 持久化` 把「持久化」从「metadata-only」升级为「真实持久化」:每个 session 绑一个 RWO PVC,runner Job 把 PVC **直接挂载**到 `${CODEX_HOME}/`,codex app-server 自己管落盘,不引入 copy 钩子。 + +#### 关键边界 + +1. **不引入 copy 路径**:PR #78 已回退 replacement 逻辑(v0.1 不接受 stale `thread/resume` 蒙混)。本节同样禁止「runner Started 后 copy/restore」「热补丁写入 PVC」「伪造 `thread/resume:completed`」等任何变体;只能 PVC 直接挂载,codex 自己读写。 +2. **硬验收 = 「PVC 不删就随时 resume」**:跨 runner pod 重建 / 跨 runner Job 重建 / 7 天长跨度(场景 D)都必须 `thread/resume:completed`,不能用 `idleTimeoutMs` 拉成永驻或 runner 不退蒙混。 +3. **profile 隔离 + 永不复用**:每个 session 一个 PVC,PVC 内容只服务当前 `backendProfile` 的 codex rollout;profile 切换必须新 sessionId + 新 PVC。 +4. **写后即一致**:`codex-rollout-storage-mounted` 是 backend adapter 在 `initialize` 之后发出的事件,含 `pvcName` / `mountPath` / `codexRolloutSubdir`;runner 通过 `POST /api/v1/sessions/:id/storage/refresh` 写回摘要。 +5. **eviction 显式化**:只有 `AGENTRUN_SESSION_PVC_NAME` 已设 + codex 返回 `no rollout found for thread id` 时升级为 `session-store-evicted`;其他 `thread/resume` 失败按 T7 走 `thread-resume-failed`。 + +#### 行为约束 + +- `POST /api/v1/runs/:runId/commands` 在 `storage_kind='evicted'` 时直接短路返回 `session-store-evicted`,不创建 runner Job。 +- runner Job manifest 必须含 `volumes: [{ name: agentrun-sessions, persistentVolumeClaim: { claimName: ... } }]` 与对应 `volumeMounts`;PVC `Phase=Bound` 才允许启动 turn。 +- codex 写 `${CODEX_HOME}//YYYY/MM/DD/rollout--.jsonl`;其他路径不进入 PVC。 +- GC 在 `expires_at` 过期且无 active run 时删 PVC;删 PVC 同时把 `storage_kind` 置为 `evicted`。 +- HWLAB Cloud Web Workbench(PR D)收到 `session-store-evicted` 时走「同 conversationId + 新 sessionId + `threadId=null`」的 reset UX,不假装续接;新 session 走 `POST /api/v1/sessions` 重新创建 PVC。 + ### P1 ResourceBundleRef / bundle materialization `ResourceBundleRef` 必须按 Git-only 模型落地:`repoUrl + full commitId` 是唯一内容身份。runner 只能 checkout 到允许 workspace 前缀,不能覆盖 `/app`、Secret projection、profile runtime home 或 session 目录。第一阶段支持 `subdir`、`sparsePaths`、`submodules=false`、`lfs=false`、`credentialRef` 的最小字段即可。HWLAB canary 只需要 `pikasTech/HWLAB` 固定 full commit 的普通 checkout;用户上传文件和对象存储 artifact 不进入 `v0.1`。 @@ -213,6 +233,7 @@ HWLAB 旧 Code Agent 的业务 prompt 和 skill 注入必须迁移为 `ResourceB | trace/result 元语 | 已实现最小合同 | 新增 run/command result envelope,聚合 terminal status、reply、failureKind、event cursor、artifact summary、attempt、SessionRef 和 ResourceBundleRef 摘要。 | | cancel | 已实现最小闭环 | 已提供 run/command cancel API;pending cancel 会阻止新 runner Job,running runner 通过轮询触发 backend abort,终态写入 event、command state 和 run status。 | | SessionRef | 已实现最小持久化 | run 可携带 `sessionRef`,manager 保存 session/thread,runner 会按 threadId resume,result envelope 暴露脱敏 session 摘要;TTL/GC 仍按后续运维策略细化。 | +| SessionRef | v0.1.1 实现中/未发布 | 在「metadata-only 最小持久化」基础上把 session 真实持久化:每个 session 绑 RWO PVC(`agentrun-v01-session-`),runner Job 把 PVC 直接挂到 `${CODEX_HOME}/`,codex app-server 自己落盘;HWLAB Cloud Web Workbench 收到 `session-store-evicted` 走 reset UX,**不**写 fake 续接。 | | ResourceBundleRef | 已实现 Git-only materialization/待 promptRefs 与 skillRefs | run 可携带 `repoUrl + full commitId`,runner checkout 到 `AGENTRUN_WORKSPACE_ROOT` 下的隔离目录并记录 commit/tree/workspace 摘要;`toolAliases` 已实现;上传文件和对象存储仍不进入 v0.1;HWLAB 初始 prompt 与 skill 注入按本规格待补齐。 | | 同 run/runner 多 turn | 已实现最小闭环 | runner Job 在 idle timeout 内持续 poll 同一 run 的后续 command;普通 turn completed 不终结 run,bundle 只 materialize 一次,command result 按 commandId 独立聚合。 | | HWLAB v0.2 canary | 待实现 | 需要 HWLAB dispatcher adapter 调 AgentRun 手动调度 API,并转换 result/trace。 | diff --git a/docs/reference/spec-v01-runtime-assembly.md b/docs/reference/spec-v01-runtime-assembly.md index dd1af90..c25ae19 100644 --- a/docs/reference/spec-v01-runtime-assembly.md +++ b/docs/reference/spec-v01-runtime-assembly.md @@ -127,6 +127,27 @@ HWLAB v0.2 原有 Code Agent 已经验证了 profile、session、workspace 和 S - runner 启动时,有 SessionRef 则执行 `thread/resume`,没有 SessionRef 则执行 `thread/start`;profile 切换不得复用另一 profile 的 session。 - v0.1 先定义边界;持久 session store、TTL、GC 和 resume 验收按 [spec-v01-hwlab-manual-dispatch.md](spec-v01-hwlab-manual-dispatch.md) 分阶段推进。 +#### v0.1.1 SessionRef 持久化(per-session RWO PVC 直接挂载) + +`v0.1` 的 P0 把 SessionRef 收敛为「只持久化 `sessionId`/`threadId`/metadata」。`v0.1.1` 必须把 SessionRef 升级为真正持久化,让 `codex app-server thread / resume` 跨 runner pod 重建继续成功,且「PVC 不删就随时 resume」是硬验收。 + +边界: + +- 每个 session 绑定一个 RWO PVC:`agentrun-v01-session-`,固定 namespace `agentrun-v01`、StorageClass 走 `AGENTRUN_SESSION_STORAGE_CLASS`(默认 `local-path`)、默认容量 1Gi。 +- runner Job 必须在 `${CODEX_HOME}/` 位置把 PVC **直接挂载**到 runner pod;`auth.json`、`config.toml`、`state_*.sqlite`、`memories/`、`tmp/`、`skills/` 仍由 `emptyDir` 父目录提供,不持久化。 +- 完整 CODEX_HOME 不进入 PVC,profile Secret 投影仍走 `agentrun-v01-runner-secret-reader`,与 session 持久化路径分离。 +- runner 启动时把 PVC 名、namespace、mount path、codex rollout subdir 写入 env(`AGENTRUN_SESSION_PVC_NAME`、`AGENTRUN_SESSION_PVC_NAMESPACE`、`AGENTRUN_SESSION_PVC_MOUNT_PATH`、`AGENTRUN_CODEX_ROLLOUT_SUBDIR`),backend adapter 不需要直接读 k8s API。 +- `codex_rollout_subdir` 走 env,默认 `sessions`,codex CLI 升级改子目录时只改该 env,不改 runner/backend 装配。 +- `POST /api/v1/sessions` 创建 session 时同步创建 PVC;`POST /api/v1/runs/:runId/commands` 命中已有 session 且 `storage_kind='evicted'` 时短路返回 `session-store-evicted`,不创建 runner Job。 +- GC 循环(默认 5min,可调 `AGENTRUN_SESSION_GC_INTERVAL_MS`)扫到 `expires_at` 过 + 无 active run 的 session 删 PVC。 +- runner Job 启动后把 PVC 摘要(`pvcName`/`pvcPhase`/`storage_size_bytes`/`storage_files_count`/`storage_sha256`/`storage_updated_at`)通过 `POST /api/v1/sessions/:id/storage/refresh` 写回 manager;manager 不读 PVC 内容,只读摘要。 + +禁止路径: + +- 不引入「runner 启动后 copy/restore」path,撤掉的 copy 方案不允许复活(PR #78 已回退 replacement 逻辑,本节同样禁止任何「持久化卷 + 热修脚手架」变体)。 +- 不允许 fake `thread/resume:completed`,PVC 真的存在且 codex 真的能 resume 才算通过。 +- 不写 fallback、不写 replacement、不拼接历史。`thread/resume` 失败时按 [spec-v01-backend-codex.md](spec-v01-backend-codex.md) T7 直接失败;只有 `AGENTRUN_SESSION_PVC_NAME` 已设 + codex 报 `no rollout found for thread id` 时升级为 `session-store-evicted`,用于上层区分「PVC 被删」与「真协议错误」。 + ### ResourceBundleRef - P0 固定 Git-only,由 `repoUrl + full commitId` 决定内容身份。 @@ -261,5 +282,6 @@ HWLAB v0.2 原有 Code Agent 已经验证了 profile、session、workspace 和 S | `BackendImageRef` | 部分实现 | CI/CD 已使用 digest-pinned runtime image;当前 runner/backend 仍复用 agentrun 镜像。 | | `ProfileRef` | 已实现/待 MiniMax-M3 主闭环 | `codex` 与 `deepseek` 已通过 SecretRef、writable runtime home 和真实 stdio turn 验证;`minimax-m3` 已进入 profile/SecretRef 装配,需要完成真实 CLI 手动验收。 | | `SessionRef` | 已实现最小持久化 | manager 持久化 `sessionId/conversationId/threadId`,run 创建会解析既有 session,runner 按 threadId resume;session 不保存 credential 文件,TTL/GC 后续细化。 | +| `SessionRef` | v0.1.1 实现中/未发布 | manager 持久化 `sessionId/conversationId/threadId` + 每个 session 绑 RWO PVC(`agentrun-v01-session-`),runner Job 把 PVC 直接挂到 `${CODEX_HOME}/`,codex app-server 自己落盘;不允许 copy/restore,不允许 replacement threadId;TTL/GC 由 mgr 周期任务管理。 | | `ResourceBundleRef` | 已实现 Git-only materialization/待 promptRefs 与 skillRefs 实现 | `repoUrl + full commitId` 已进入 run schema 和 runner checkout,workspace 受 `AGENTRUN_WORKSPACE_ROOT` 限制,event/result 记录 commit/tree/workspace 摘要;`toolAliases` 已实现,`promptRefs` thread-start 注入和 `skillRefs` registry 聚合待实现。 | | `toolCredentials` | 已实现最小 env projection | GitHub PR 和 UniDesk SSH passthrough 等 agent shell/tool 授权通过装配 SPEC 的 SecretRef 进入 runner;v0.1 支持 `tool=github` 与 `tool=unidesk-ssh`、`projection.kind=env`,runner Job 使用 `valueFrom.secretKeyRef` 注入,不用 `transientEnv` 绕过。 | diff --git a/docs/reference/spec-v01-services.md b/docs/reference/spec-v01-services.md index d34b77c..dbf7dfe 100644 --- a/docs/reference/spec-v01-services.md +++ b/docs/reference/spec-v01-services.md @@ -90,6 +90,19 @@ Runner inbound API 只允许本地或私有诊断,不作为业务客户端入 | Tenant policy boundary | Run schema 合同 | 保留,P0 | 作为 `Run` 的必填字段和最小校验存在,不做独立 policy engine;tenant 的业务授权仍由 UniDesk/HWLAB 判定。 | 并入 `spec-v01-agentrun-mgr.md` | | Observability | 最小事件/日志合同 | 保留,P1 子项 | 作为 manager/runner 的 event、terminal status、failureKind、logPath 和 redaction 最小合同,不拆独立观测系统。 | 并入 `spec-v01-agentrun-mgr.md`、`spec-v01-agentrun-runner.md` | | `agentrun-scheduler` | 长驻调度器 | Deferred | M1-M3 稳定后再实现自动 pending scan、capacity selection 和 runner Job 创建。 | `spec-v01-scheduler.md` | + +### v0.1.1 Session state 持久化 + +`v0.1` 的 `P1 SessionRef 持久化` 已从「metadata-only」升级为「per-session RWO PVC 直接挂载」: + +- `agentrun-mgr`:负责 session 创建时同步创建 PVC(`POST /api/v1/sessions`)、删 PVC(`DELETE /api/v1/sessions/:id/storage`)、runner 上报摘要(`POST /api/v1/sessions/:id/storage/refresh`)、GC 循环、运行前 PVC 存在性检查。 +- `agentrun-runner`:Job manifest 渲染 `agentrun-sessions` PVC volume,把 PVC 直接挂到 `${CODEX_HOME}/`,env 透传 PVC name / namespace / mount path / codex rollout subdir。 +- `codex-app-server-stdio` backend:在 `initialize` 之后 emit `codex-rollout-storage-mounted` 事件;`AGENTRUN_SESSION_PVC_NAME` 已设 + `thread/resume` 返回 `no rollout found` 时升级为 `session-store-evicted`。 +- 接受方(HWLAB Cloud Web Workbench):收到 `session-store-evicted` 时走 reset UX,**不**写入 fake 续接。 + +Manager SA RBAC 增量:`persistentvolumeclaims: [create, get, list, watch, delete]`。Runner SA 不需新增 PVC 相关权限(挂载走 Pod spec)。PVC storage class / size / 命名规则由 env 收敛(`AGENTRUN_SESSION_STORAGE_CLASS`、`AGENTRUN_SESSION_STORAGE_SIZE`)。 + +禁止路径:fake app-server mock 通过;强制重发同 sessionId 同 threadId 蒙混;`thread/resume:no rollout found` 改写为 `thread/start` + replacement threadId(PR #78 已否决);`idleTimeoutMs` 拉成永驻当成本特性;runner Job 启动后再做 copy/restore(撤掉的路径,禁止复活)。 | AgentRun Queue | 系统能力 | 新增,P0 规格 | 直接吸收 UniDesk Code Queue 的队列能力;第一版只做 RESTful API、CLI 和轻量轮询,不做 Event/OA/integrations,不迁移历史数据。 | `spec-v01-queue.md` | | 多 backend 路由 | 系统能力 | Deferred | `v0.1` 不做跨 backend kind 的自动路由和调度;仅支持同一 Codex stdio backend kind 下的 `codex`/`deepseek`/`minimax-m3` profile 手动选择。旧 MiniMax/OpenCode 直连路线不进入 Queue 首版。 | 后续版本 spec | | UI | 前端 | Deferred | `v0.1` 不要求独立 UI;UniDesk/HWLAB canary 可通过 CLI/API 验证。 | 后续版本 spec | diff --git a/scripts/src/gitops-render.ts b/scripts/src/gitops-render.ts index a84cf90..7ec50cd 100644 --- a/scripts/src/gitops-render.ts +++ b/scripts/src/gitops-render.ts @@ -342,6 +342,9 @@ rules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["create", "get", "list", "watch", "delete"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding diff --git a/src/common/types.ts b/src/common/types.ts index 4335eb1..72436e9 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -22,6 +22,7 @@ export type FailureKind = | "provider-invalid-tool-call" | "provider-unavailable" | "infra-failed" + | "session-store-evicted" | "cancelled"; export type RunStatus = "pending" | "claimed" | "running" | "completed" | "failed" | "blocked" | "cancelled"; diff --git a/src/mgr/postgres-store.ts b/src/mgr/postgres-store.ts index 89c3975..1739906 100644 --- a/src/mgr/postgres-store.ts +++ b/src/mgr/postgres-store.ts @@ -175,6 +175,26 @@ CREATE INDEX IF NOT EXISTS agentrun_sessions_version_idx ON agentrun_sessions (v CREATE INDEX IF NOT EXISTS agentrun_runs_session_idx ON agentrun_runs ((session_ref->>'sessionId'), updated_at DESC); `; +const sessionStateStorageMigrationSql = ` +ALTER TABLE agentrun_sessions ADD COLUMN IF NOT EXISTS storage_kind text NOT NULL DEFAULT 'none'; +ALTER TABLE agentrun_sessions ADD COLUMN IF NOT EXISTS storage_pvc_name text; +ALTER TABLE agentrun_sessions ADD COLUMN IF NOT EXISTS storage_namespace text; +ALTER TABLE agentrun_sessions ADD COLUMN IF NOT EXISTS storage_size_bytes bigint; +ALTER TABLE agentrun_sessions ADD COLUMN IF NOT EXISTS storage_files_count integer; +ALTER TABLE agentrun_sessions ADD COLUMN IF NOT EXISTS storage_sha256 text; +ALTER TABLE agentrun_sessions ADD COLUMN IF NOT EXISTS storage_updated_at timestamptz; +ALTER TABLE agentrun_sessions ADD COLUMN IF NOT EXISTS codex_rollout_subdir text NOT NULL DEFAULT 'sessions'; +ALTER TABLE agentrun_sessions ADD COLUMN IF NOT EXISTS storage_pvc_phase text; +ALTER TABLE agentrun_sessions ADD COLUMN IF NOT EXISTS storage_evicted_at timestamptz; + +CREATE INDEX IF NOT EXISTS agentrun_sessions_storage_pvc_idx + ON agentrun_sessions (storage_pvc_name) + WHERE storage_pvc_name IS NOT NULL; + +CREATE INDEX IF NOT EXISTS agentrun_sessions_storage_kind_idx + ON agentrun_sessions (storage_kind, expires_at); +`; + const hwlabManualDispatchMigrationSql = ` ALTER TABLE agentrun_runs ADD COLUMN IF NOT EXISTS session_ref jsonb; ALTER TABLE agentrun_runs ADD COLUMN IF NOT EXISTS resource_bundle_ref jsonb; @@ -297,6 +317,11 @@ const postgresMigrations: MigrationDefinition[] = [ checksum: checksumSql(sessionControlMigrationSql), sql: sessionControlMigrationSql, }, + { + id: "007_v01_session_state_storage", + checksum: checksumSql(sessionStateStorageMigrationSql), + sql: sessionStateStorageMigrationSql, + }, ]; export function postgresMigrationContract(): JsonRecord { diff --git a/src/selftest/cases/00-redaction-postgres.ts b/src/selftest/cases/00-redaction-postgres.ts index 870461a..4388d83 100644 --- a/src/selftest/cases/00-redaction-postgres.ts +++ b/src/selftest/cases/00-redaction-postgres.ts @@ -13,7 +13,9 @@ const selfTest: SelfTestCase = async () => { (error) => error instanceof AgentRunError && error.failureKind === "infra-failed" && error.message.includes("DATABASE_URL is required"), ); const postgresContract = postgresMigrationContract(); - assert.equal(postgresContract.latestMigrationId, "006_v01_session_control"); + assert.equal(postgresContract.latestMigrationId, "007_v01_session_state_storage"); + assert.equal((postgresContract.migrationIds as string[]).includes("007_v01_session_state_storage"), true); + assert.ok(typeof (postgresContract.checksums as Record)["007_v01_session_state_storage"] === "string" && (postgresContract.checksums as Record)["007_v01_session_state_storage"].length > 0); assert.equal((postgresContract.checksums as Record)["002_v01_backend_profiles"], "928b5c490cc4539cb64ecef34784557601b2724fa2870570f16a53576804e49c"); assert.ok(Array.isArray(postgresContract.requiredTables)); assert.ok(postgresContract.requiredTables.includes("agentrun_schema_migrations"));