尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PicoClaw Session 系统架构解析:从路由作用域到 JSONL 持久化的完整链路

PicoClaw Session 系统架构解析:从路由作用域到 JSONL 持久化的完整链路 PicoClaw Session 系统架构解析从路由作用域到 JSONL 持久化的完整链路【免费下载链接】picoclawTiny, Fast, and Deployable anywhere — automate the mundane, unleash your creativity项目地址: https://gitcode.com/gh_mirrors/pi/picoclaw本文深入解析 PicoClaw 运行时的 Session会话系统它如何把入站消息映射到稳定会话作用域、如何以 JSONL 格式跨 turn 与跨进程重启持久化上下文又如何在运行时全面采用不透明 canonical key 的同时兼容旧版agent:...session key。读完本文你将掌握pkg/session、pkg/memory、pkg/agent三层的核心链路理解会话维度的分配规则、崩溃恢复语义与迁移回退机制并能在实际部署中正确配置session.dimensions与 dispatch 规则。本文内容基于当前仓库 docs/architecture/session-system.zh.md不讨论 web/backend/middleware 中 launcher 登录 Cookie 或 dashboard 鉴权 session。Session 系统的四大职责Session 系统在 PicoClaw 运行时中承担四件核心事务决定消息归属判断哪些消息应当共享同一段上下文同一段历史记忆持久化上下文让这段上下文能够跨 turn、跨进程重启持续存在抽象存储接口向 agent loop 暴露一个足够小的SessionStore接口屏蔽底层后端差异兼容旧格式在存储层与路由层迁移期间继续兼容旧版 session keyagent:...形式。主要组件总览整个会话链路由五个层次构成从抽象的接口定义到具体的磁盘文件层次文件作用Session 抽象pkg/session/session_store.go定义 agent loop 依赖的SessionStore接口。旧后端pkg/session/manager.go每个 session 一个 JSON 文件的旧实现仍作为回退方案保留。Session 适配层pkg/session/jsonl_backend.go把pkg/memory.Store适配成SessionStore并支持 alias 与 scope metadata。持久化存储pkg/memory/jsonl.goAppend-only JSONL 存储与.meta.json元数据侧文件。Scope / Key 构建pkg/session/scope.go、pkg/session/key.go、pkg/session/allocator.go从路由结果生成结构化 scope、不透明 canonical key 和 legacy alias。运行时集成pkg/agent/instance.go、pkg/agent/agent_utils.go、pkg/agent/agent_message.go初始化存储、分配 session scope并在 turn 执行前落 metadata。SessionStore 接口agent loop 的唯一依赖SessionStore接口pkg/session/session_store.go#L13-L33只暴露与 agent loop 相关的持久化操作追加消息AddMessage/AddFullMessage、读取历史GetHistory、读写摘要GetSummary/SetSummary、替换历史SetHistory、逻辑截断TruncateHistory、保存Save、枚举会话ListSessions与释放资源Close。值得注意的设计细节是写方法Add*、Set*、Truncate*都是 fire-and-forget 的不返回错误实现内部自行记录失败日志。这是为了匹配旧SessionManager的原有契约让 agent loop 的调用方代码无需关心底层存储失败——任何满足该接口的后端都可以被无缝替换。Session 数据模型结构化作用域 SessionScope结构化的会话身份由session.SessionScope表示pkg/session/scope.go#L7-L14字段含义VersionScope 模式版本当前为ScopeVersionV1值为1。AgentID处理该 turn 的路由 agent。Channel归一化后的入站 channel 名称为空时回退为unknown。Account归一化后的 bot / account 标识。Dimensions当前启用的隔离维度顺序例如chat或sender。Values每个维度对应的具体归一化值。Allocator 当前只识别四个维度spacechattopicsender默认配置是按 chat 共享上下文pkg/config 中SessionDimensions的默认值对应配置片段为{ session: { dimensions: [chat] } }也就是说默认情况下同一个 chat 内的所有消息共享同一段上下文如果 dispatch rule 覆盖了维度则以 rule 为准详见下文「路由策略如何决定维度」。ScopeVersionV1常量定义在 pkg/session/scope.go#L3-L4SessionScope还提供了CloneScope深拷贝方法供 metadata 读取时安全返回给调用方避免上层误改内部状态。Canonical Key 与 Legacy Alias不透明 canonical key运行时现在优先使用不透明 canonical key格式为sk_v1_sha256它由 pkg/session/key.go 计算得到BuildSessionKey(scope)先调用CanonicalScopeSignature(scope)pkg/session/key.go#L183-L200把 scope 序列化为稳定的字符串再交给BuildOpaqueSessionKey做 SHA-256 摘要并加上sk_v1_前缀pkg/session/key.go#L25-L32。canonical signature 的构成如下vversion|agentagent_id|channelchannel|accountaccount|dimvalue|...其中维度部分按Dimensions的顺序拼接所有字段都会做小写化与 trim。这样设计的好处是存储 key 稳定可复算同时不再把持久化格式与某一种旧文本 key 绑定死未来即使改变 legacy 格式canonical key 也能保持稳定。Legacy alias为了兼容旧数据allocator 还会生成 legacy alias例如agent:main:direct:user123 agent:main:slack:channel:c001 agent:main:pico:direct:pico:session-123这些 alias 很重要因为旧 session、部分测试以及某些工具仍然会引用这种格式。构建函数包括 pkg/session/key.go#L96-L109 的BuildLegacyDirectAliasesdirect 场景生成三条最短的agent:agent:direct:peer、带 channel 的、带 channelaccount 的与 pkg/session/key.go#L111-L123 的BuildLegacyPeerAlias群聊/频道场景生成agent:agent:channel:peerKind:peerID。所有 alias 都会去重、转小写。JSONL backend 会在读写前先把 alias 解析回 canonical keyResolveSessionKey。此外如果调用方已经显式传入了受支持的 session keyagent loop 会保留它不强行改成新分配的 routed key——这条逻辑在 pkg/agent/agent_utils.go#L364-L369 的resolveScopeKey中func resolveScopeKey(routeSessionKey, msgSessionKey string) string { if isExplicitSessionKey(msgSessionKey) { return msgSessionKey } return routeSessionKey }其中「显式 key」的判断由 pkg/session/key.go#L44-L46 的IsExplicitSessionKey完成涵盖两类格式不透明 canonical keysk_v1_前缀IsOpaqueSessionKeylegacyagent:...keyIsLegacyAgentSessionKey分配流程从入站消息到会话读写普通入站消息的完整链路如下InboundMessage - RouteResolver.ResolveRoute(...) - session.AllocateRouteSession(...) - resolveScopeKey(...) - ensureSessionMetadata(...) - AgentLoop turn 执行 - SessionStore 读写具体来说pkg/agent/agent_message.go 先用归一化后的 inbound context 解析 agent routesession.AllocateRouteSessionpkg/session/allocator.go#L32-L43把 route 的SessionPolicy和 inbound context 组合成结构化SessionScopeAllocator 会生成四类产物Allocation结构pkg/session/allocator.go#L14-L20SessionKey当前路由会话的 canonical keySessionAliases该路由会话的兼容 aliasMainSessionKeyagent 级主会话 key由 legacy main alias 哈希而来MainAliases主会话对应的 legacy alias即agent:agent:mainrunAgentLoop通过ensureSessionMetadatapkg/agent/agent_utils.go#L399-L410持久化 scope metadata 和 alias后续读写时JSONLBackend.ResolveSessionKey会先把 alias 映射回 canonical key。MainSessionKey与普通聊天会话是分开的它主要服务于 agent 级、系统级的上下文场景比如processSystemMessage。从源码看主会话 alias 为agent:agent_id:mainpkg/session/key.go#L85-L87其 canonical 形式由BuildMainSessionKey生成。路由策略如何决定维度RouteResolver在解析路由时会附带生成SessionPolicypkg/routing/route.go#L11-L15包含Dimensions与IdentityLinks。其取值逻辑在 pkg/routing/route.go#L101-L110func (r *RouteResolver) sessionPolicy(rule *config.DispatchRule) SessionPolicy { dimensions : r.cfg.Session.Dimensions if rule ! nil len(rule.SessionDimensions) 0 { dimensions rule.SessionDimensions } return SessionPolicy{ Dimensions: normalizeSessionDimensions(dimensions), IdentityLinks: cloneIdentityLinks(r.cfg.Session.IdentityLinks), } }也就是说默认使用全局session.dimensions一旦某条 dispatch rule 显式声明了session_dimensions则以 rule 为准。normalizeSessionDimensions会把维度名小写化并过滤掉space/chat/topic/sender之外的未知值pkg/routing/route.go#L112-L124。相关测试见 pkg/config/config_test.goTestDefaultConfig_SessionDimensions等。Scope 构建规则pkg/session/allocator.go 的buildSessionScope会从归一化后的 inbound context 生成 scope 值pkg/session/allocator.go#L45-L113关键规则如下space变成space_type:space_idtype 为空时回退为spacechat变成chat_type:chat_idtype 为空时回退为directtopic变成topic:topic_idsender会先经过session.identity_links归一化再写入此外还有两个需要单独记住的特殊规则。Telegram forum 隔离Telegram forum topic 必须默认保持隔离即使配置只写了chat维度。为此如果消息来自 Telegram forum 且策略里没有显式包含topicallocator 会把/topic_id拼到chat值后面判断函数为shouldPreserveTelegramForumIsolationpkg/session/allocator.go#L150-L164。例如group:-1001234567890/42 group:-1001234567890/99这两者会得到不同的 session key——同一群组的不同 forum 子主题各自拥有独立上下文互不串扰。Identity linkssession.identity_links可以把多个 sender 标识折叠为一个 canonical identity。核心实现是CanonicalSessionIdentityIDpkg/session/key.go#L127-L136与resolveLinkedPeerIDpkg/session/key.go#L146-L181它会把原始 sender ID 及其带 channel 前缀的形式、冒号后的部分都作为候选与 identity_links 配置中的条目匹配命中则替换为 canonical 名称。dispatch 匹配和 session 分配都会使用这套映射因此同一个人即使跨 channel 或 account 使用不同原始 sender ID也可以继续落到同一段上下文里。存储格式JSONL 主文件 metadata 侧文件默认运行时后端是pkg/memory.JSONLStorepkg/memory/jsonl.go#L65-L77外面包了一层session.JSONLBackend。每个 session 使用两类文件{sanitized_key}.jsonl {sanitized_key}.meta.json各自保存.jsonl一行一个providers.Messageappend-only.meta.json摘要、时间戳、行数、逻辑截断偏移、scope、aliasessanitizeKeypkg/memory/jsonl.go#L101-L106会把 session key 中的:、/、\全部替换为_这样 Telegram forum 的chatID/threadID、Slack 的channel/thread_ts等复合 ID 不会产生子目录也兼容 Windows 文件名规则同时与pkg/session的 sanitize 逻辑保持一致保证迁移路径对齐。SessionMeta当前包含pkg/memory/jsonl.go#L43-L52字段JSON 键含义Keykey原始 session keySummarysummary会话摘要Skipskip逻辑截断偏移被跳过的行数Countcount已记录的消息行数CreatedAtcreated_at创建时间UpdatedAtupdated_at更新时间Scopescope结构化 scope 的原始 JSONpkg/memory保持与上层解耦Aliasesaliaseslegacy alias 列表写入与崩溃语义宁可读到旧数据不要丢数据JSONL store 的设计核心是「追加优先、宁可暂时读到旧数据也不要丢数据」AddMessage/AddFullMessage先追加一行 JSON再fsync最后更新 metadatapkg/memory/jsonl.go#L593-L652。注意其中的f.Sync()调用——注释明确指出没有 Sync 的话断电后 append 可能只停留在内核 page cache重启即丢失TruncateHistory先做逻辑截断本质上只是推进meta.Skippkg/memory/jsonl.go#L711-L747。它不会物理删除任何行GetHistory读取时按Skip跳过前面的行Compact才会真正重写 JSONL 文件把被跳过的旧行物理移除pkg/memory/jsonl.go#L796-L833并通过WriteFileAtomic临时文件 fsync rename原子替换SetHistory和Compact都先写 metadata 再改写 JSONL如 pkg/memory/jsonl.go#L778-L787 注释所说明如果中途崩溃meta 已是Skip0而旧文件仍完整GetHistory会从头读取最多「多读到」已截断的旧消息但不会丢数据下次SetHistory/Compact会纠正读取 JSONL 时如果碰到损坏行例如崩溃产生的半截写会记录日志并跳过该行而不是让整个 session 读取失败pkg/memory/jsonl.go#L512-L525读取器还设置了 10 MB 的单行上限以容纳read_file、web search 等大型工具结果。一个容易被误解的语义变化JSONLBackend.Save对应到底层的store.Compact(...)pkg/session/jsonl_backend.go#L176-L182。也就是说Save在新实现里不再是「把内存脏数据刷盘」——因为每次写入都已fsync落盘——而是「在逻辑截断后回收无效行占用的磁盘空间」若没有待回收的跳过行则直接返回 no-op。并发模型固定 64 分片锁pkg/memory.JSONLStore使用固定 64 分片 mutex按 session key 的 FNV-32a hash 做串行化pkg/memory/jsonl.go#L82-L86。这样既能做到「按 session 串行」又不会因为 session 数量增长而把 mutex map 做成无界结构——这对长时间运行的守护进程很重要。另外在 alias 提升、SetHistory等需要同时操作两个 key 的场景lockSessionPairpkg/memory/jsonl.go#L363-L384会按 key 字典序加锁避免死锁。旧的SessionManager则是一个内存 map 加 RW mutex。这两个实现都满足同一个SessionStore接口所以 agent loop 不需要写任何存储后端特化逻辑。兼容与迁移JSONL 优先失败回退pkg/agent/instance.go#L483-L504 的initSessionStore会优先初始化 JSONL 后端启动过程如下创建memory.NewJSONLStore(dir)执行memory.MigrateFromJSON(...)pkg/memory/migration.go#L31把旧.jsonsession 迁入新格式用session.NewJSONLBackend(store)包装如果 JSONL 初始化或迁移失败则回退到session.NewSessionManager(dir)。这个回退是刻意设计的做一半的迁移比整轮继续使用旧后端更危险。注释明确说明迁移失败意味着该目录下存储无法可靠写入若继续使用 JSONL 会出现「部分 session 在 JSONL、部分仍在 JSON」的分裂状态所以宁可整体回退到旧后端跑一轮。Alias 提升PromoteAliasHistory第一次为 canonical key 建 metadata 时EnsureSessionMetadatapkg/session/jsonl_backend.go#L66-L96会先UpsertSessionMeta写入 scope 与 aliases再调用PromoteAliasHistory尝试把某个非空 legacy alias 的历史提升到 canonical sessionpkg/memory/jsonl.go#L237-L262。但这件事只会在canonical session 仍然为空时发生因此不会覆盖已经存在的 canonical 历史sessionHasVisibleContentLocked会先检查。提升过程还做了两项防护主会话 alias 不会被提升isMainSessionAliaspkg/memory/jsonl.go#L267-L290同时识别 legacy 形式agent:main:main精确三段与 opaque 形式对agent:main:main/agent:Main:main/agent:MAIN:main的哈希因为主会话是共享的全局回退把它提升进各个会话会把陈旧消息附加到每个 Web UI 新会话上注释引用了 issue #2972写入失败可回滚promoteAliasHistoryLockedpkg/memory/jsonl.go#L386-L445在重写 JSONL 前会先保留原始字节若随后写 meta 失败则用restoreRawJSONL回滚。这保证了系统在迁移到 opaque key 的同时仍能保留旧历史例如旧的 direct-message key旧的 Pico direct-session keyResolveSessionKeypkg/memory/jsonl.go#L295-L353则是 alias 映射的核心对于不含:、/、\的简单 key 且文件直接存在时走短路返回否则扫描目录下所有.meta.json若入参命中某个 meta 的Aliases列表则返回其Keycanonical key还支持 meta.Key 精确匹配与「存在直接文件」的兜底。其他 SessionStore 实现ephemeralSessionStorepkg/agent/subturn.go#L608-L701 里定义了ephemeralSessionStore。它同样实现SessionStore但只存在于内存里在 sub-turn 结束时销毁Save与Close均为空实现。这样 SubTurn 就能复用相同的 session 接口而不会把子任务历史写进父会话的持久存储——父子会话上下文清晰隔离。运行时消费者不止 agent loopSession 系统不只被 agent loop 使用web/backend/api/session.go 通过GET /api/sessions读取 JSONL metadata 和旧 JSON session并把历史暴露给 launcher UI注意其读取器与共享 JSONL store 保持相同的 10 MB 行大小限制pkg/agent/agent_steering.go 可以在 steering 场景下恢复 scope metadata因为 alias 解析发生在 agent loop 之下JSONLBackend 层测试和工具仍然可以继续使用 legacy alias无需感知底层 key 格式的演进。相关文件索引pkg/session/session_store.gopkg/session/manager.gopkg/session/jsonl_backend.gopkg/session/scope.gopkg/session/key.gopkg/session/allocator.gopkg/memory/jsonl.gopkg/memory/migration.gopkg/agent/instance.gopkg/agent/agent_utils.gopkg/agent/agent_message.gopkg/routing/route.go若想了解会话维度配置与 launcher 的关系可进一步阅读 docs/architecture/README.md 与 config/example.json会话维度的默认值验证测试见 pkg/config/config_test.go 中的TestDefaultConfig_SessionDimensions。【免费下载链接】picoclawTiny, Fast, and Deployable anywhere — automate the mundane, unleash your creativity项目地址: https://gitcode.com/gh_mirrors/pi/picoclaw创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表