ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Onyx Craft 会话保活:Kubernetes 沙箱中 opencode 会话历史持久化的完整实现指南

Onyx Craft 会话保活:Kubernetes 沙箱中 opencode 会话历史持久化的完整实现指南 Onyx Craft 会话保活Kubernetes 沙箱中 opencode 会话历史持久化的完整实现指南【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer导读本文深入剖析 Onyxdanswer开源 AI 平台中 Craft 会话的 opencode 会话历史持久化方案。Craft 会话以opencode serve作为沙箱内的长生命周期 Agent 运行时而 Kubernetes 沙箱一旦休眠、被驱逐或重建仅靠 Postgres 中保存的BuildSession.opencode_session_id无法找回 opencode 的会话历史——历史行存放在沙箱文件系统里。本文将带你理解 Onyx 如何通过「沙箱全局 opencode 历史快照 签名 Sidecar 端点 FileStore 持久化 启动恢复门控」这套机制让 Agent 会话在 Pod 休眠、恢复与重建后依然可续接同时保持 API Server 持有持久化存储的所有权、普通会话快照聚焦于会话本地文件。读完你将掌握其存储模型、完整的 Provision/Restore 与 Snapshot 流程、发送消息时的乐观会话 ID 解析策略以及重置、删除、闲置休眠等生命周期路径的设计取舍。背景为什么opencode_session_id本身不够在 Onyx Craft 的架构中opencode serve是沙箱内长期运行的 Agent 运行时负责驱动每一轮用户消息的 Agent 推理与工具调用。Onyx 会把BuildSession.opencode_session_id持久化到 Postgres以便后续轮次复用同一个 opencode 会话而不是每条消息都新建一个。但仅有这个 ID 是不够的。原因在于 opencode 的会话数据行存储在opencode 自己的数据目录里——即沙箱文件系统的opencoce数据根目录Kubernetes 下默认OPENCODE_DATA_HOME/workspace/opencode-data。当 Kubernetes 沙箱 Pod 休眠sleep、被驱逐evicted或被重新创建recreated时Pod 本地卷上的数据会丢失如果沙箱级别的 opencode 数据没有被持久化并在启动时恢复Postgres 里保存的 ID 就指向了一个不存在的数据——后续轮次将无法续接任何历史上下文。因此该实现将 opencode 历史作为沙箱全局状态进行持久化与普通的「按会话隔离的工作区快照」区分开来。相关设计文档位于 preserve-opencode-sessions.md本文以该文档为骨架并结合仓库源码逐层展开。设计目标整个方案围绕以下目标展开跨生命周期保留历史opencode 会话历史在 Kubernetes 沙箱休眠、恢复和重新供给reprovision后依然完整可用。存储所有权归属 API Server / FileStore 层沙箱 Pod 不持有任何租户存储凭据持久化读写全部由 API Server 侧负责。保持普通会话快照聚焦普通会话快照只负责每个会话的本地文件outputs/、attachments/不携带 opencode 数据。Docker 不承担该能力Docker 工作区快照不携带 opencode 数据opencode 历史持久化当前是 Kubernetes 专属能力。乐观替换策略当恢复后的数据库里保存的 opencode ID 缺失时主动铸造一个新 ID 并持久化后续再通过 follow-up 把已保存的聊天历史回放到新会话中。会话删除的语义边界删除 Onyx BuildSession 时删除产品可见记录并在沙箱运行期间尽力删除实时 opencode 会话失败不阻塞 Onyx 删除也不会清理持久化的历史归档。存储模型两套持久化面方案在持久化上明确区分了两个层面源码中对应SnapshotManager与 Sidecar 快照端点两条路径。按会话隔离的工作区快照普通会话快照只捕获会话本地用户产出outputs/attachments/存在且非空时这些快照刻意不包含.opencode-data。归档的创建与恢复由沙箱 Sidecar 通过以下 HTTP 端点完成POST /snapshot/createPOST /snapshot/restore/{session_id}Sidecar 拥有 Pod 本地文件系统的读写权而 API Server 通过SnapshotManager把归档流转存进 FileStore。端点常量定义在 contract.pySIDECAR_HEALTH_PATH /health SIDECAR_READY_PATH /ready SIDECAR_SNAPSHOT_CREATE_PATH /snapshot/create SIDECAR_SNAPSHOT_RESTORE_ROUTE f{SIDECAR_SNAPSHOT_RESTORE_PREFIX}/{{session_id}} SIDECAR_OPENCODE_HISTORY_CREATE_PATH /opencode-history/create SIDECAR_OPENCODE_HISTORY_RESTORE_PATH /opencode-history/restore SIDECAR_OPENCODE_HISTORY_MARK_RESTORED_PATH /opencode-history/mark-restored沙箱全局的 opencode 历史opencode 历史由沙箱内所有 BuildSession共享。在 Kubernetes 中Pod 配置了OPENCODE_DATA_HOME/workspace/opencode-dataopencode 数据根目录为/workspace/opencode-data持久化到 FileStore 的对象路径按沙箱确定性生成见 snapshot_manager.py 的opencode_history_storage_pathsandbox-snapshots/{tenant_id}/{sandbox_id}/opencode-history.tar.gz归档内部使用稳定的归档根目录.opencode-data/这样一来opencode 持久化与会话工作区彻底分离同时避免设计一个「按会话定制的 opencode store」——那与 opencode 实际采用的沙箱级数据模型不符。关键组件拆解SnapshotManager统一 FileStore 持久化入口SnapshotManager 是两类快照共用的 FileStore 持久化层普通 Sidecar 创建的工作区快照沿用其归档与未压缩大小校验opencode 历史快照使用上述确定性存储路径且不受到工作区快照大小上限的约束Kubernetes 仍通过emptyDir.sizeLimit限制 Pod 本地 opencode 数据卷的大小。与普通快照每次生成随机snapshot_id不同opencode 历史快照的 FileStore 键是确定性的、按沙箱唯一写入时使用reject_emptyTrue拒绝空流persist_opencode_snapshot_from_stream并支持has_opencode_history_snapshot查询与delete_opencode_history_snapshot删除后者幂等文件缺失不报错。Sandbox Sidecar 快照端点Sidecar HTTP 服务实现在 server.py它把 Pod 本地文件系统操作暴露为签名 HTTP 端点Sidecar 不上传 S3、不持有租户存储凭据所有写路径请求都需要X-Push-SignatureEd25519与X-Push-Timestamp校验_verify_signature校验时间戳漂移不超过 60 秒并对{timestamp}|{path}|{sha256_hex}消息验签。与 opencode 历史相关的端点GET /ready只有启动恢复路径完成或显式跳过opencode 历史恢复后才返回健康该端点用作可重启 init Sidecar 的启动门控而非稳态 Pod 的就绪信号ready 实现 在未恢复时返回 503。POST /opencode-history/createopencode 数据目录无内容时返回204否则以application/gzip流式返回归档。POST /opencode-history/restore接收经过签名、SHA-256 哈希校验的归档请求体X-Bundle-Sha256头见_spool_verified_archive在 Pod 本地恢复 opencode 数据目录。POST /opencode-history/mark-restored当不存在持久化历史快照时标记一个全新沙箱已就绪。Opencode 历史归档辅助模块opencode_history.py 是 opencode 数据归档逻辑的归属模块与专注于普通会话工作区快照的snapshot.py分离。其关键职责与实现细节路径安全_safe_opencode_data_dir拒绝符号链接、拒绝非目录路径、拒绝逃逸/workspace根目录的路径L28-L43。保持数据路径独立opencode 数据路径位于/workspace/opencode-data在/workspace/sessions之外。归档前暂存stagingcreate_opencode_history_archive_file先把数据目录复制到临时暂存根.opencode-data/再打 tar.gzgzipcompresslevel6。SQLite 一致性备份_snapshot_sqlite_db_if_present用sqlite3.Connection.backup()把暂存中的opencode/opencode.db替换为一致性副本PRAGMA busy_timeout5000只读源连接即使opencode serve正在运行归档也携带连贯的 DB 快照L72-L83。安全恢复恢复时使用 Python 标准库 tar 的data过滤器tar.extractall(staging_path, filterdata)随后用os.replace原子地替换/workspace/opencode-data内容为解压出的.opencode-data/根忽略归档中.opencode-data/之外无关的顶层条目。损坏 DB 兜底恢复后若已知的opencode/opencode.db存在但损坏非 SQLite magic、PRAGMA quick_check非ok、或路径为符号链接/非文件见_opencode_db_is_healthy则清空恢复出的 opencode 数据目录让opencode serve全新启动而不是对着已知的坏状态启动。启动恢复标记mark_opencode_history_restored在 Sidecar 托管的受管状态目录写入标记文件/workspace/managed/.onyx/opencode-history-restoredmode0o600opencode_history_restored据此判断恢复门控是否放行。并发保护全部归档/恢复操作通过threading.RLock串行化恢复在opencode_history_restored()已为真时直接返回幂等。Kubernetes 沙箱管理器kubernetes_sandbox_manager.py 协调 Pod 生命周期、Sidecar 调用、FileStore 流式传输与启动恢复门控。它是后端中唯一声明该能力的实现Docker 管理器当前未启用此能力supports_opencode_history_persistence True声明位于 kubernetes_sandbox_manager.py基类默认值为False见 base.py。Craft 的 Kubernetes Pod 模板使用原生可重启 init SidecarinitContainers[*].restartPolicy: Always因此 Craft Helm 部署要求Kubernetes 1.33 或更新版本。这一要求由 Chart 在部署/渲染阶段强制校验ENABLE_CRAFTtrue搭配SANDBOX_BACKENDkubernetes在旧集群上渲染即失败并非运行时后端版本检查。相关部署说明可参考 sandbox/README.md。Provision 与 Restore 流程当 Kubernetes 沙箱 Pod 启动时恢复流程严格保证opencode serve不会在恢复完成或被显式跳过之前启动。完整时序如下Kubernetes 启动防火墙 init 容器firewall init container。Kubernetes 启动可重启的sidecarinit 容器。此时其健康端点/health可用但启动端点/ready保持阻塞。K8s 管理器确保沙箱 Service 存在并发布 not-ready Pod 地址使 Sidecar 在 Pod ready 之前即可被访问。若不存在持久化 opencode 历史快照管理器向/opencode-history/mark-restored发送签名请求显式标记「跳过恢复」。若存在持久化历史快照API Server 从 FileStore 读取归档到临时文件计算其 SHA-256然后 POST 到/opencode-history/restore对应源码 restore_opencode_history_snapshot其中hashlib.file_digest(..., sha256)计算摘要后经post_archive上传。Sidecar 恢复 opencode 数据目录若恢复出的当前 opencode DB 损坏则清空该数据让 opencode 全新启动。Sidecar 标记 opencode 历史已恢复。Sidecar/ready端点成功释放可重启 init Sidecar 的启动门控。Kubernetes 启动sandbox应用容器。其 entrypoint 以XDG_DATA_HOME指向/workspace/opencode-data的方式运行opencode serve。K8s 管理器等待 Pod ready 与opencode serveready。关键不变量是opencode serve绝不会在恢复完成或被显式跳过之前启动。当 K8s 管理器发现已存在健康的沙箱 Pod 时直接复用该 Pod不会重新执行启动历史恢复。快照创建流程opencode 历史快照在沙箱休眠之前以及尽力恢复best-effort recovery期间创建K8s 管理器对/opencode-history/create发送签名请求request_and_stream_new_snapshot见 create_opencode_history_snapshot。Sidecar 检查 opencode 数据目录是否有内容。无内容Sidecar 返回204管理器保留任何已存在的持久化历史归档。有内容Sidecar 暂存 opencode 数据目录用sqlite3.Connection.backup()替换暂存中的 SQLite DB生成 tar.gz 归档并流式返回。API Server 把响应流交给SnapshotManager。SnapshotManager存储到稳定的沙箱级 FileStore 键sandbox-snapshots/{tenant_id}/{sandbox_id}/opencode-history.tar.gz。第 3 步的语义很关键当 Sidecar 因实时存储为空而返回204时管理器必须保留既有归档。这保护了闲置/恢复路径——一次瞬时的「实时为空/缺失」不应摧毁最后一次已知良好的历史。发送消息流程乐观的会话 ID 解析Prompt 路径刻意采用乐观策略保证「恢复后的沙箱里找不到已保存 opencode ID」不会让用户轮次失败会话行携带BuildSession.opencode_session_id若已持久化。每轮之前_ensure_opencode_session_id在行内无 ID 时铸造并持久化一个新的 opencode 会话 ID。yield_sandbox_events以已保存 ID 与on_opencode_session_resolved回调调用sandbox_manager.send_message。_send_message_via_serve调用OpencodeServeClient.ensure_sessionserve_client.pyGET /session/{id}预检404 则POST /session新建。若已保存 ID 存在opencode 返回200复用同一 ID。若 opencode 返回404Onyx 创建全新 opencode 会话并触发回调更新 BuildSession 行_persist_resolved_id经_persist_opencode_session_id写回新 ID见 streaming.py。非 404 的查找错误仍然抛出——运行时故障不应静默铸造替代会话。消息发送到解析后的 opencode 会话正常事件流式传输继续。这种设计的取舍在于恢复后的沙箱缺失 opencode ID 时用户轮次不会失败会开启新 opencode 会话并记录新 ID代价是 opencode 本身不会在新会话里收到先前的聊天历史——「历史回放」明确不在本次变更范围内见下文「已知后续工作」。删除会话流程删除 BuildSession 会删除 Onyx 的持久化会话记录当沙箱运行中且行内存在opencode_session_id时Onyx 还会尽力请求删除该实时 opencode 会话DELETE /session/{id}见 serve_client.py。该清理仅是优化失败仅记录日志不阻塞 Onyx 行删除。opencode 历史仍是沙箱全局的实现数据因此会话删除不会裁剪持久化历史归档。如果 opencode 中仍保留已删除 BuildSession 的行该行成为孤儿数据无法再通过 Onyx 触达。具体步骤SessionManager.delete_session获取会话 prompt 槽位。若沙箱运行中且 BuildSession 有 opencode 会话 ID管理器尽力删除该实时 opencode 会话。继续普通工作区清理与 Snapshot FileStore 清理。删除 BuildSession DB 行。对于休眠或未运行的沙箱删除不会尝试编辑或校验 opencode 历史Onyx 行被移除后留在持久化历史归档中的陈旧 opencode 记录就是孤儿实现数据。闲置休眠流程沙箱清理任务处理闲置运行中的沙箱若后端支持 opencode 历史持久化先尝试创建 opencode 历史快照。若快照失败但沙箱仍通过健康检查任务让沙箱继续运行——对健康沙箱直接休眠而没有新鲜历史会冒着丢失最近 Agent 上下文的风险。若 Pod 已不可达任务记录警告并继续清理——此时实时文件系统不可信、无法为新鲜快照访问。随后任务对每个会话工作区做快照。必需快照完成后沙箱才能休眠。对应逻辑见 sandbox_lifecycle.py先create_opencode_history_snapshot异常时若health_check通过则跳过休眠。恢复流程Recovery当 Onyx 检测到不健康的运行中沙箱需要终止/恢复时生命周期代码在终止前尽力创建一次 opencode 历史快照sandbox_lifecycle.py 的snapshot_opencode_history_best_effort。此路径是 best-effort 的因为沙箱可能已经部分死亡失败会被记录但不会永久阻塞恢复。重新供给时正常的恢复流程会恢复最后一次持久化的 opencode 历史快照若存在。重置 / 全新开始流程用户请求的沙箱重置是破坏性的「start fresh」操作删除持久化 opencode 历史快照delete_opencode_history_snapshot幂等。终止沙箱资源。在调用方持有的事务中将沙箱 DB 行标记为TERMINATED。仅在持久化历史删除与沙箱终止都成功后才提交。注意持久化 FileStore 删除在 DB 事务之外如果历史删除成功后终止失败API 报告重置失败并回滚 DB 状态但持久化历史对象已不存在——这保留了「下次成功供给时 start fresh」的不变量。若沙箱行已是TERMINATED重置没有可保护的实时 Pod 或 DB 状态转换此时持久化历史删除是 best-effort失败记录日志重置仍返回成功后续重置可在 FileStore 恢复后重试删除。这一点与闲置休眠刻意不同休眠保留历史重置移除历史。为什么它不是普通会话快照普通快照循环遍历会话目录、为每个会话存储其工作区——这对 outputs 与 attachments 是正确的模型。但 opencode 历史不同opencode 把所有会话存放在一个沙箱级数据存储中按会话归档无法安全地表示共享数据存储多个 BuildSession 可共享同一个 opencode 历史存储删除一个会话会留下孤儿 opencode 行Onyx 不编辑 opencode 内部存储重置必须删除共享归档而不是合并或保留按会话存储。因此实现复用了同一套高层快照基础设施SnapshotManager、FileStore、签名 Sidecar 流式传输但把 opencode 历史作为沙箱级归档配有自己的 create/restore 端点与策略。这一决策完整记录在 preserve-opencode-sessions.md 的 Why This Is Not A Normal Session Snapshot 章节。运维不变量清单沙箱 Pod 不持有持久化存储凭据API Server 拥有 FileStore 读写。Sidecar 拥有 Pod 本地文件系统读写。普通会话快照不包含.opencode-data。opencode 历史存放在/workspace/sessions树之外的沙箱全局卷上。opencode 历史快照包含完整的.opencode-data/归档根。恢复出的损坏opencode/opencode.db会被丢弃改用全新 opencode 数据目录。opencode serve只在启动历史恢复完成后启动。Craft Helm 部署在 Kubernetes 版本低于 1.33 时快速失败在开始供给沙箱 Pod 之前。发送消息时缺失已保存 opencode ID → 铸造新会话并持久化404之外的运行时查找错误仍使该轮失败。Craft Helm 部署在ENABLE_CRAFTtrue搭配非 Kubernetes 沙箱后端时快速失败。会话删除尽力移除实时 opencode 会话但清理失败仍删除 BuildSession 行。会话删除不修改持久化 opencode 历史归档。重置在终止沙箱之前删除持久化 opencode 历史避免「已终止沙箱还带着可恢复的陈旧历史」。已知后续工作Follow-Up当前实现中若恢复出的沙箱不包含已保存的 opencode IDOnyx 会铸造新会话并持久化避免阻塞用户但新会话尚未包含先前的聊天历史。计划中的后续工作是检测这种「替换会话」场景并在发送下一条用户 prompt 之前把已保存的 BuildMessage 历史回放到 opencode 中。该回放逻辑应位于底层快照/恢复路径之上——快照层继续在可能时恢复 DB、保持存储导向的职责边界。源码阅读清单若想深入实现按以下文件顺序阅读opencode_history.py归档创建/恢复、SQLite 一致性备份、损坏 DB 兜底、恢复标记。server.pySidecar 签名端点与/ready启动门控。contract.py全部端点路径与请求模型常量。snapshot_manager.pyFileStore 持久化、确定性历史存储路径、幂等删除。kubernetes_sandbox_manager.pyPod 生命周期协调、恢复门控、能力声明。serve_client.pyensure_session/delete_session/send_message的 HTTP 语义。serve_transport.py消息发送预检与会话 ID 解析回写。streaming.py_ensure_opencode_session_id与_persist_opencode_session_id。manager.pydelete_session等会话生命周期入口。sandbox_lifecycle.py闲置休眠与恢复前的 best-effort 历史快照。sandbox/README.md部署模式与 Kubernetes 1.33 版本要求的总体说明。小结Onyx Craft 的 opencode 会话历史持久化是一个「职责分离 乐观容错」的典型案例API Server 通过SnapshotManager独占 FileStore 所有权Sidecar 只做签名保护的 Pod 本地文件系统操作恢复流程以/ready门控保证opencode serve绝不先于数据就绪启动发送消息时的乐观 ID 解析、休眠/恢复/重置各自的策略共同保证了用户在 Pod 生命周期抖动下依然能继续对话。这套设计也为后续「把已保存聊天历史回放进替换会话」留下了清晰的演进空间。【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表