Rollout 文件和 thread-store 持久化
这一阶段像系统的“记事本和档案柜”,在幕后把对话长期保存好,重启后还能找回。rollout 负责把每次会话写成文件、压缩旧记录、列出和搜索历史,还用名字索引本加快查找。message-history 另存全局消息流水,避免多窗口写乱。thread-store 则规定统一的存取接口,并提供本机实现:写入时先落盘再更新索引,读取、分页列表、搜索、删除时在文件和数据库之间核对,尽量避免记录不一致。
Rollout crate 界面
这些文件定义 rollout 子系统的公共 API,并将主应用配置桥接到 rollout 专用类型和辅助工具。
core/src/rollout.rs源码 ↗
rollout 可以理解成“把一次对话或任务过程存成可追踪记录”的那套功能。这个文件本身不真正读写会话文件,也不做复杂计算,它更像插座转换器:一边接 core 里的 Config,另一边接独立的 codex_rollout 库。codex_rollout 不需要知道 core::Config 里面字段怎么摆,只要通过 RolloutConfigView 这个接口问:“Codex 的家目录在哪?”“SQLite 数据目录在哪?”“当前工作目录在哪?”“用哪个模型提供方?”“要不要生成记忆?”这里的 5 个方法就负责把答案从 Config 里拿出来。文件还把很多 codex_rollout 的类型和函数重新导出,方便 core 其他地方用同一个入口拿到会话列表、查找线程、读取摘要等能力。
Config::codex_home27–29 ↗
fn codex_home(&self) -> &std::path::Path
作用:把配置里的 Codex 主目录交给 rollout 功能使用。这个目录通常是存放会话、配置或相关本地数据的根位置。
数据流:进去的是一个已经加载好的 Config;函数读取其中的 codex_home 字段,把它转换成普通路径引用;出来的是这个路径本身,不会新建目录,也不会改配置。
调用关系:当 codex_rollout 通过 RolloutConfigView 需要知道“会话文件该去哪里找或写”时,会调用这个方法。它只是提供地址,真正的查找、读取、写入工作由 codex_rollout 里的功能继续完成。
Config::sqlite_home31–33 ↗
fn sqlite_home(&self) -> &std::path::Path
作用:把配置里的 SQLite 数据目录告诉 rollout 功能。SQLite 是一种本地文件数据库,常用来把结构化数据存在一个文件里。
数据流:进去的是 Config;函数取出 sqlite_home 字段并作为路径引用返回;结果只是一个位置说明,不会打开数据库,也不会检查文件是否存在。
调用关系:rollout 相关代码如果需要知道本地数据库放在哪里,会通过 RolloutConfigView 问到这里。这个方法负责回答位置,后续是否连接数据库、读写数据,是别的模块的事情。
Config::cwd35–37 ↗
fn cwd(&self) -> &std::path::Path
作用:提供当前工作目录。当前工作目录就是程序此刻默认“站在哪个文件夹里”办事。
数据流:进去的是 Config;函数读取 cwd 字段,返回一个路径引用;它不切换目录,也不访问磁盘,只是把已经记录好的位置交出去。
调用关系:rollout 功能需要把某次会话和当时所在项目目录关联起来时,会用这个方法取目录。它处在配置和会话记录之间,负责把上下文位置传给后续记录逻辑。
Config::model_provider_id39–41 ↗
fn model_provider_id(&self) -> &str
作用:提供当前使用的模型提供方标识。简单说,就是告诉 rollout 这次对话背后用的是哪一家或哪一种模型服务。
数据流:进去的是 Config;函数读取 model_provider_id 这个字符串,并返回字符串引用;它不会验证这个名字,也不会连接模型服务。
调用关系:会话记录或元数据需要标明模型来源时,codex_rollout 会通过统一接口调用它。这个方法只交出名字,真正把名字写入记录或用于展示的是 rollout 的其他部分。
Config::generate_memories43–45 ↗
fn generate_memories(&self) -> bool
作用:告诉 rollout 是否应该启用“生成记忆”的开关。这里的记忆可以理解为从会话里提炼出的、以后可能复用的信息。
数据流:进去的是 Config;函数读取 memories.generate_memories 这个布尔值;出来的是 true 或 false,表示开还是关,不会自己生成任何记忆。
调用关系:当 rollout 或相关会话流程需要决定要不要记录、触发记忆生成时,会问这个方法。它提供决策开关,后续真正生成或跳过记忆的动作由别的逻辑执行。
rollout/src/lib.rs源码 ↗
rollout 这部分代码主要管 Codex 的会话记录文件:放在哪里、怎么读、怎么写、怎么列出来、怎么搜索、什么时候压缩归档。这个文件本身不做具体活儿,更像一家商场的一楼导览牌:告诉外部代码可以去哪些柜台办事,并把常用柜台直接摆到门口。它定义了会话文件夹名,比如普通会话放在 sessions,归档会话放在 archived_sessions。它还定义了一组“交互式会话来源”,也就是哪些来源算用户正在对话,比如命令行、VS Code、atlas、chatgpt。其余大量 pub use 是把下层模块里的重要类型和函数重新出口,外部使用者不用知道内部文件怎么分,只要从 rollout 这个库拿就行。这样做的好处是减少依赖方对内部结构的了解,内部模块以后搬家,外部代码也不一定要跟着改。
Rollout 存储基础组件
这些文件提供 rollout 文件、sidecar 命名索引、全局消息历史以及已存转录文本搜索的底层持久化机制。
message-history/src/lib.rs源码 ↗
这个文件就是“聊天记录本”的读写层。它把每条历史写成一行 JSON(一种常见的文本数据格式),放在 ~/.codex/history.jsonl 里;一行就是一条记录,里面有会话编号、时间戳和文本。写入时它会先把整行准备好,再拿文件锁。文件锁可以理解成“谁拿到笔谁先写”,防止两个程序同时往同一个本子上写导致字挤在一起。它还会把文件权限设成只有本人能读写,避免历史内容被同机器上的别人看到。文件太大时,它会从最旧的记录开始删,但会保留最新写入的那条。读取方面,它能告诉外面:当前历史文件是哪一个、里面大概有多少条;也能按第几条把某条历史取出来。这个文件既关心安全,也关心并发写入和文件大小。
HistoryConfig::new70–76 ↗
fn new(codex_home: impl Into<PathBuf>, history: &History) -> Self
作用:把外部的历史配置整理成本文件能直接使用的配置。别人只要给它 Codex 的主目录和历史设置,它就生成一个保存路径、保存策略、大小上限都齐全的小配置包。
数据流:进去的是 codex_home,也就是 Codex 的家目录位置,以及 History 配置;它把目录转成路径,把“是否保存历史”和“最多保存多大”复制出来;出来的是一个 HistoryConfig,后面写入、读取、查元数据都会靠它找文件和判断规则。
调用关系:测试和会话相关代码会先调用它做配置,例如写历史、查历史、填充会话历史信息时都需要这个配置。它本身不读写文件,只是给 append_entry、history_metadata、lookup 这些函数准备好统一的路线图。
调用图:被 6 处调用(append_entry_trims_history_to_soft_cap, append_entry_trims_history_when_beyond_max_bytes, append_message_history_entry, lookup_message_history_entry, session_configured_populates_history_metadata, thread_session_state_from_thread_response);外部调用 1 个(into)。
history_filepath79–81 ↗
fn history_filepath(config: &HistoryConfig) -> PathBuf
作用:算出历史文件的完整路径。它把 Codex 主目录和固定文件名 history.jsonl 拼起来,避免各处自己拼路径拼错。
数据流:进去的是 HistoryConfig;它读取里面的 codex_home,再接上固定的历史文件名;出来的是类似 ~/.codex/history.jsonl 的路径。
调用关系:append_entry 写入前会用它找文件,history_metadata 统计前会用它找文件,lookup 查单条记录前也会用它找文件。它是所有历史文件访问的共同入口。
调用图:被 3 处调用(append_entry, history_metadata, lookup)。
append_entry98–183 ↗
async fn append_entry(
text: &str,
conversation_id: impl std::fmt::Display,
config: &HistoryConfig,
) -> Result<()>
作用:把一条新消息追加到历史文件末尾。它会尊重配置:如果用户选择不保存历史,就什么也不写;如果允许保存,就安全地写入一整行记录。
数据流:进去的是消息文本、会话编号和历史配置;它先看配置是否允许保存,再创建目录、生成当前时间、把会话编号和文本做成一行 JSON;然后打开历史文件,确认权限,拿到独占文件锁后一次性写到文件末尾;最后如果配置了大小上限,还会修剪旧记录。出来通常只是成功或失败的结果,同时磁盘上的历史文件可能多了一条新记录,也可能被裁掉一些旧记录。
调用关系:这是写历史的主流程。它会先找 history_filepath 算路径,再调用 ensure_owner_only_permissions 确认隐私权限;真正可能卡住线程的文件锁和写入放进 spawn_blocking,意思是交给专门的阻塞任务去做,别拖慢异步运行时。写完后会让 enforce_history_limit 检查文件是否太大。
调用图:调用 2 个内部函数(ensure_owner_only_permissions, history_filepath);外部调用 6 个(to_string, new, to_string, now, create_dir_all, spawn_blocking)。
enforce_history_limit189–262 ↗
fn enforce_history_limit(file: &mut File, max_bytes: Option<usize>) -> Result<()>
作用:在历史文件超过大小上限时,从最老的记录开始删,给文件瘦身。它的重点是保住最新写入的记录,不让刚写进去的内容马上被删掉。
数据流:进去的是已经打开的历史文件和可选的最大字节数;如果没有上限、上限为 0、或文件还没超限,它直接结束;如果超限,它逐行统计每行有多长,算出要删掉多少开头内容,再把剩下的尾部内容读出来,清空原文件并写回尾部。出来是成功或错误,文件内容会从“太长的一本流水账”变成“只保留较新的尾巴”。
调用关系:它通常在 append_entry 已经拿到写锁并写完新记录后运行,这样修剪时不会和别的写入互相打架。它会调用 trim_target_bytes 来决定目标大小,不是刚好剪到硬上限,而是剪到稍小一点,避免下一次写入立刻又要修剪。
调用图:调用 1 个内部函数(trim_target_bytes);外部调用 13 个(new, flush, metadata, seek, set_len, try_clone, write_all, Start, new, new (+3 more))。
trim_target_bytes264–270 ↗
fn trim_target_bytes(max_bytes: u64, newest_entry_len: u64) -> u64
作用:计算历史文件修剪后应该留多大。它不是只剪到最大值,而是剪到最大值的 80% 左右,让文件有一点缓冲空间。
数据流:进去的是最大允许字节数和最新一条记录的长度;它算出 80% 的软目标,再确保这个目标至少能装下最新一条记录;出来的是最终要保留的目标字节数。
调用关系:enforce_history_limit 在决定删多少旧记录时会调用它。它像一个“裁剪尺子”,告诉修剪流程:别剪太少,也别把最新记录剪没。
调用图:被 1 处调用(enforce_history_limit)。
history_metadata279–282 ↗
async fn history_metadata(config: &HistoryConfig) -> (u64, usize)
作用:告诉外面当前历史文件的身份和记录数量。这样调用方可以知道历史文件有没有变、里面大概有多少条可查。
数据流:进去的是历史配置;它先用 history_filepath 找到文件路径,再把具体读取工作交给 history_metadata_for_file;出来的是一对数字:文件身份编号和行数。如果文件不存在或读不了,通常返回 0 或行数 0。
调用关系:这是给外部使用的异步接口。它隐藏了路径细节,调用方不用知道历史文件具体叫什么,只要拿着配置来问就行;真正查看磁盘和数换行符的工作由 history_metadata_for_file 完成。
调用图:调用 2 个内部函数(history_filepath, history_metadata_for_file)。
lookup294–297 ↗
fn lookup(log_id: u64, offset: usize, config: &HistoryConfig) -> Option<HistoryEntry>
作用:按“文件身份”和“第几条”查一条历史记录。它用来确保你查的是同一个历史文件,而不是文件被轮换或重建后位置已经变了。
数据流:进去的是期望的文件身份编号、从 0 开始的记录位置、以及历史配置;它先算出历史文件路径,再交给 lookup_history_entry 打开文件、校验身份、读取对应行并解析;出来是找到的 HistoryEntry,找不到或出错就是 None。
调用关系:这是给外部调用的同步查找入口。它自己只做路径转换,真正的加共享锁、逐行读取、解析 JSON 都交给 lookup_history_entry。如果调用方在异步环境里用它,注释提醒应该放到阻塞任务里运行。
调用图:调用 2 个内部函数(history_filepath, lookup_history_entry)。
ensure_owner_only_permissions317–319 ↗
async fn ensure_owner_only_permissions(_file: &File) -> Result<()>
作用:确保历史文件只有当前用户能读写,保护聊天记录隐私。在 Unix 系统上它会把权限设成 0o600,意思是“本人可读写,别人不行”;Windows 上则直接视为成功。
数据流:进去的是已经打开的历史文件;在 Unix 上,它读取文件当前权限,如果不是只允许本人读写,就复制文件句柄并在线程池里修改权限;出来是成功或错误,文件权限可能被收紧。Windows 版本不改文件,直接返回成功。
调用关系:append_entry 在写入历史前会调用它。这样每次创建或打开历史文件时,都能顺手检查隐私设置,避免历史文件因为旧权限或系统默认权限而暴露。
调用图:被 1 处调用(append_entry);外部调用 3 个(metadata, try_clone, spawn_blocking)。
history_metadata_for_file321–348 ↗
async fn history_metadata_for_file(path: &Path) -> (u64, usize)
作用:直接查看某个历史文件,拿到它的身份编号和里面有多少行记录。它是 history_metadata 背后的实际干活函数。
数据流:进去的是一个文件路径;它先读取文件元数据,从中拿到文件身份编号,然后打开文件,分块读取内容并数里面有多少个换行符;出来的是 (文件身份编号, 记录数量)。如果文件不存在或元数据读失败,返回 (0, 0);如果身份读到了但内容打不开,返回 (身份编号, 0)。
调用关系:history_metadata 会调用它。它会用 log_identity 从系统元数据里提取“这个文件是谁”,并用 memchr_iter 快速数换行符;换行符数量就代表 JSON Lines 文件里的记录条数。
调用图:调用 1 个内部函数(log_identity);被 1 处调用(history_metadata);外部调用 3 个(open, metadata, memchr_iter)。
lookup_history_entry350–417 ↗
fn lookup_history_entry(path: &Path, log_id: u64, offset: usize) -> Option<HistoryEntry>
作用:在一个具体历史文件里查找指定位置的那一行,并把它变回一条历史记录。它会先确认文件身份,防止拿旧位置去读一个已经变化的文件。
数据流:进去的是文件路径、期望的文件身份编号和行号位置;它打开文件,读取元数据并算出当前身份;如果身份不匹配就返回空;匹配后尝试拿共享锁,也就是“允许多人读,但别和写入冲突”的锁;拿到锁后逐行读文件,找到目标行后用 JSON 解析成 HistoryEntry。出来是找到的记录,或者在打不开、读错、解析失败、位置不存在时返回 None 并写警告日志。
调用关系:lookup 会调用它做实际查找。它会调用 log_identity 校验文件身份;如果文件正被别人独占写入,它会短暂等待并重试,避免读到一半被改动的内容。
调用图:调用 1 个内部函数(log_identity);被 1 处调用(lookup);外部调用 4 个(new, new, sleep, warn!)。
log_identity432–434 ↗
fn log_identity(_metadata: &std::fs::Metadata) -> Option<u64>
作用:从文件元数据里取出一个能代表“这是不是同一个文件”的编号。Unix 上用 inode(文件在文件系统里的身份证号),Windows 上用创建时间。
数据流:进去的是文件元数据;在 Unix 上它读取 inode,在 Windows 上读取创建时间,在其他系统上可能没有合适办法就返回空;出来是一个可选的数字身份编号。
调用关系:history_metadata_for_file 用它告诉外部当前历史文件的身份,lookup_history_entry 用它检查查找时的文件是否还是同一个。它是防止“文件换了但行号还被误用”的小保险。
调用图:被 2 处调用(history_metadata_for_file, lookup_history_entry);外部调用 2 个(creation_time, ino)。
rollout/src/compression.rs源码 ↗
rollout 文件可以理解成一行一条记录的日志本,文件多了、旧了以后会占空间。这个文件做两件事:后台挑出很久没改过的普通 .jsonl 文件,把它们压成 .jsonl.zst;前台读文件时自动判断该读普通文件还是压缩文件,像打开同一本书一样使用。为了避免出错,它会用临时文件、运行标记和“文件有没有被改过”的检查,防止两个压缩任务撞车,也防止正在写的文件被误删。需要追加内容时,它还能把压缩文件重新解压回普通文件。里面还有一组统计上报函数,用来记录压缩了多少、跳过多少、花了多久。
spawn_rollout_compression_worker29–31 ↗
fn spawn_rollout_compression_worker(codex_home: PathBuf)
作用:启动一个尽力而为的后台压缩工人,用来把本地很久不用的 rollout 文件压小。调用方不用等它完成,启动失败也不会阻塞主流程。
数据流:输入是 codex_home 目录路径 → 它把路径交给内部 worker 模块的 spawn → 输出没有返回值,只是在后台尝试开始压缩任务。
调用关系:这是外部进入压缩后台任务的入口,自己只做转交;真正的启动细节交给 worker::spawn。
调用图:外部调用 1 个(spawn)。
file_modified_time34–41 ↗
async fn file_modified_time(path: &Path) -> io::Result<Option<time::OffsetDateTime>>
作用:查一个 rollout 文件最后被修改的时间,而且普通文件和压缩文件都能查。这样上层排序或展示时不用自己判断文件是哪种形态。
数据流:输入是一个期望的文件路径 → 先找磁盘上实际存在的是普通 .jsonl 还是 .jsonl.zst → 再读文件信息并取修改时间 → 返回可能存在的时间,找不到就返回空。
调用关系:它会调用 existing_rollout_path 找真实文件,再调用文件元数据读取;上层的 file_modified_time 和 file_modified_time_utc 会用它做时间查询。
调用图:被 2 处调用(file_modified_time, file_modified_time_utc);外部调用 2 个(existing_rollout_path, metadata)。
open_rollout_line_reader47–58 ↗
async fn open_rollout_line_reader(path: &Path) -> io::Result<RolloutLineReader>
作用:打开一个按行读取 rollout 的读者对象,不管文件是否压缩都能用同一个接口读。它还会短暂重试,避免文件刚好在压缩或解压切换时被误判为不存在。
数据流:输入是想打开的路径 → 多次尝试 reader::open_once,如果遇到“找不到文件”就等 50 毫秒再试 → 成功时返回 RolloutLineReader,其他错误直接返回。
调用关系:读取摘要、加载记录、搜索内容等流程会调用它;它把真正打开普通文件或压缩文件的工作交给 reader::open_once。
调用图:被 5 处调用(read_head_for_summary, read_head_summary, load_rollout_items, first_rollout_content_match_snippet, rollout_contains);外部调用 2 个(open_once, sleep)。
compressed_rollout_path62–64 ↗
fn compressed_rollout_path(path: &Path) -> PathBuf
作用:给测试代码用,把一个 rollout 路径换算成压缩后的 .jsonl.zst 路径。这样测试能准确知道压缩文件应该叫什么。
数据流:输入是路径 → 转交给 path 模块的 compressed_rollout_path → 输出对应的压缩文件路径。
调用关系:这是测试可见的小包装;调用关系里 existing_rollout_path 会间接依赖这种路径换算规则。
调用图:被 1 处调用(existing_rollout_path);外部调用 1 个(compressed_rollout_path)。
materialize_rollout_for_append67–72 ↗
async fn materialize_rollout_for_append(path: &Path) -> io::Result<PathBuf>
作用:在异步流程里准备一个可以追加写入的普通 rollout 文件。如果当前只有压缩版,它会先把压缩版解回普通文本。
数据流:输入是路径 → 复制成 PathBuf,并把耗时的解压工作丢到阻塞线程里做 → 输出最终可追加写入的普通 .jsonl 路径,或错误。
调用关系:创建新日志或追加 rollout 记录前会调用它;它把实际解压工作交给 materialize_rollout_for_append_blocking,避免卡住异步运行器。
调用图:被 2 处调用(new, append_rollout_item_to_path);外部调用 2 个(to_path_buf, spawn_blocking)。
materialize_rollout_for_append_blocking75–121 ↗
fn materialize_rollout_for_append_blocking(path: &Path) -> io::Result<PathBuf>
作用:同步地把压缩 rollout 解压回普通 .jsonl,用于需要直接写文件的地方。它保证追加写入前手里有一个普通文本文件。
数据流:输入是普通或压缩路径 → 算出普通路径;如果普通文件已存在就直接返回;如果只有压缩文件,就解压到临时文件,确认写入落盘,再安全放到目标位置并删除压缩文件 → 返回普通路径,并记录成功、缺失或失败指标。
调用关系:open_log_file 会在阻塞写入前用它;它会调用 plain_rollout_path、temp_path_for、创建目录、删除文件和压缩路径换算等底层工具。
调用图:调用 2 个内部函数(plain_rollout_path, temp_path_for);被 1 处调用(open_log_file);外部调用 4 个(materialize, compressed_rollout_path, create_dir_all, remove_file)。
persist_temp_file_noclobber123–130 ↗
fn persist_temp_file_noclobber(temp_path: &Path, destination: &Path) -> io::Result<()>
作用:把临时文件改名成正式文件,但如果正式文件已经存在就不覆盖。它像“把草稿交到收件箱,若箱里已有同名文件就算了”。
数据流:输入是临时文件路径和目标路径 → 把路径包装成 tempfile 的临时文件对象 → 尝试无覆盖持久化 → 成功或目标已存在都算成功,其他错误返回。
调用关系:它是解压落盘时的安全兜底工具,避免覆盖别人刚创建好的普通 rollout 文件。
调用图:外部调用 2 个(persist_noclobber, try_from_path)。
plain_rollout_path133–135 ↗
fn plain_rollout_path(path: &Path) -> PathBuf
作用:把一个可能带 .zst 后缀的压缩路径,换回标准的普通 .jsonl 路径。调用方用它统一文件身份。
数据流:输入是路径 → 转交给 path 模块移除压缩后缀 → 输出普通路径;如果本来就是普通路径则原样返回。
调用关系:materialize_rollout_for_append_blocking、existing_rollout_path 和 should_skip_compressed_sibling 都依赖它统一路径。
调用图:被 3 处调用(materialize_rollout_for_append_blocking, existing_rollout_path, should_skip_compressed_sibling);外部调用 1 个(plain_rollout_path)。
parse_rollout_file_name138–140 ↗
fn parse_rollout_file_name(name: &str) -> Option<&str>
作用:判断一个文件名是不是合法的 rollout 文件名,并返回它对应的普通 .jsonl 名字。它能识别普通版和压缩版。
数据流:输入是文件名字符串 → 转交给 file_name 模块检查前缀、后缀,并去掉 .zst → 输出合法的普通文件名,或空。
调用关系:这是文件发现流程的公共判断入口,避免每个扫描者都自己写一套命名规则。
调用图:外部调用 1 个(parse_rollout_file_name)。
RolloutFile::from_path159–170 ↗
fn from_path(path: PathBuf) -> Option<Self>
作用:把磁盘上发现的一个路径,转换成“一个逻辑 rollout 文件”。它会过滤掉不是 rollout 的文件,以及被普通文件覆盖的压缩兄弟文件。
数据流:输入是物理路径 → 读取文件名,解析是否合法 rollout;如果同名普通文件存在,就跳过压缩版 → 成功时输出 RolloutFile,里面保存实际路径和标准普通文件名。
调用关系:压缩扫描、按时间收集、按 ID 查找、内容搜索等目录遍历都会用它;它把命名判断交给 file_name,把普通/压缩优先级交给 path。
调用图:被 7 处调用(compress_rollouts_in_root, collect_flat_files_by_updated_at, collect_flat_rollout_files, find_rollout_path_by_id_from_filenames, collect_rollout_paths, scan_compressed_rollout_matches, scan_rollout_matches);外部调用 4 个(as_path, file_name, parse_rollout_file_name, should_skip_compressed_sibling)。
RolloutFile::path173–175 ↗
fn path(&self) -> &Path
作用:取出这个 RolloutFile 实际应该打开的磁盘路径。它可能指向普通文件,也可能指向压缩文件。
数据流:输入是 RolloutFile 自身 → 返回内部保存的 Path 引用 → 不修改任何东西。
调用关系:这是读取方拿到真实文件位置的小出口,配合 RolloutFile::from_path 的筛选结果使用。
调用图:外部调用 1 个(as_path)。
RolloutFile::plain_file_name178–180 ↗
fn plain_file_name(&self) -> &str
作用:取出这个 rollout 的标准普通文件名,用来解析时间、ID 等信息。即使实际文件是压缩版,这里也返回 .jsonl 名字。
数据流:输入是 RolloutFile 自身 → 返回内部保存的普通文件名字符串引用 → 不碰磁盘。
调用关系:目录扫描后的排序、匹配和识别流程会依赖这个稳定名字,而不是直接看物理文件名。
RolloutFile::is_compressed183–185 ↗
fn is_compressed(&self) -> bool
作用:判断这个 RolloutFile 当前指向的是不是压缩文件。压缩后台任务用它跳过已经压缩的文件。
数据流:输入是 RolloutFile 自身 → 取内部路径并检查文件名是否以 .jsonl.zst 结尾 → 输出 true 或 false。
调用关系:compress_rollouts_in_root 发现文件后会用它决定是否还需要压缩。
调用图:外部调用 2 个(as_path, is_compressed_rollout_path)。
RolloutFile::into_path188–190 ↗
fn into_path(self) -> PathBuf
作用:消费掉 RolloutFile,拿走里面的真实路径。适合后续只需要路径、不再需要这个包装对象的时候。
数据流:输入是 RolloutFile → 移出内部 PathBuf → 输出路径,原对象不再可用。
调用关系:压缩扫描确认要处理普通文件后,会用它把路径交给压缩任务。
RolloutLineReader::next_line205–220 ↗
async fn next_line(&mut self) -> io::Result<Option<String>>
作用:从 rollout 里读下一行记录。无论底层是普通文件还是压缩文件,调用方都用这一种方法。
数据流:输入是可变的读者对象 → 如果是普通文件就异步读下一行;如果是压缩文件,就把阻塞式解压读行放到专门线程里做 → 输出下一行字符串、文件结束的空值,或错误。
调用关系:open_rollout_line_reader 返回的对象靠它逐行消费内容;压缩文件路径会走 spawn_blocking,避免解压读取卡住异步任务。
调用图:外部调用 2 个(other, spawn_blocking)。
worker::CompressionRunMarker::try_claim274–302 ↗
fn try_claim(codex_home: &Path) -> io::Result<Option<Self>>
作用:尝试拿到一次压缩运行的“占位牌”。有了它,才能启动本轮压缩,防止同一个目录里多个压缩工人同时开工。
数据流:输入是 codex_home 目录 → 在 .tmp 下创建锁文件;如果已存在,就检查是否太旧,太旧则删掉重建 → 输出 Some 标记表示拿到资格,None 表示已有任务或刚跑过,错误则返回错误。
调用关系:worker::run 启动时首先调用它;它会创建目录、创建标记文件、读元数据和删除过期标记。
调用图:外部调用 6 个(join, new, create_run_marker_file, create_dir_all, metadata, remove_file)。
worker::CompressionRunMarker::new304–309 ↗
fn new(path: PathBuf) -> Self
作用:创建一个新的运行标记对象,并默认设置为离开作用域时自动删除锁文件。这样失败或中途退出时不会长期占坑。
数据流:输入是锁文件路径 → 放进 CompressionRunMarker,并设置 remove_on_drop 为 true → 输出标记对象。
调用关系:try_claim 成功创建锁文件后用它包装路径。
worker::CompressionRunMarker::persist311–313 ↗
fn persist(mut self)
作用:告诉运行标记:这次正常跑完后不要删除锁文件。留下它可以表示最近已经跑过,避免太频繁压缩。
数据流:输入是标记对象本身 → 把 remove_on_drop 改成 false → 不返回数据,但改变对象析构时的行为。
调用关系:worker::run 成功完成压缩后调用它,让锁文件保留下来作为“最近跑过”的记录。
worker::CompressionRunMarker::drop317–321 ↗
fn drop(&mut self)
作用:当运行标记对象被释放时,按需要清理锁文件。它是失败时自动收拾现场的保险。
数据流:输入是即将销毁的标记对象 → 如果 remove_on_drop 为 true,就尝试删除锁文件 → 不返回结果,删除失败也忽略。
调用关系:这是 Rust 的 Drop 清理钩子;try_claim 和 run 创建的标记最终都会经过这里。
调用图:外部调用 2 个(as_path, remove_file)。
worker::spawn324–341 ↗
worker::run343–393 ↗
async fn run(codex_home: PathBuf) -> io::Result<()>
作用:执行一次完整的后台压缩流程。它负责拿锁、清临时文件、扫描目录、压缩旧文件、记录统计和收尾。
数据流:输入是 codex_home → 先 try_claim 拿运行标记;拿不到就跳过;拿到后清理过期临时文件,扫描 archived sessions 和 sessions 目录,统计扫描、压缩、跳过、失败数量 → 成功时记录完成指标并保留运行标记,失败时记录失败指标并返回错误。
调用关系:worker::spawn 在后台任务中调用它;它串起 cleanup_stale_temps、compress_rollouts_in_root、指标记录和日志输出。
调用图:外部调用 11 个(now, as_path, join, debug!, info!, run, run_duration, try_claim, default, cleanup_stale_temps (+1 more))。
worker::create_run_marker_file395–407 ↗
worker::compress_rollouts_in_root409–485 ↗
async fn compress_rollouts_in_root(
root: &Path,
started_at: Instant,
stats: &mut CompressionStats,
) -> io::Result<()>
作用:扫描一个根目录下的所有 rollout 文件,并把符合条件的普通文件交给压缩任务。它会限制同时压缩的数量,避免机器压力太大。
数据流:输入是根目录、开始时间和统计对象 → 深度遍历目录;遇到合法普通 rollout 就计数并派发阻塞压缩任务;任务太多时先收一部分结果;最后等全部任务结束 → 更新统计并返回成功或错误。
调用关系:worker::run 对 sessions 和 archived sessions 两个根目录调用它;它用 RolloutFile::from_path 识别文件,用 collect_next_compression_job 和 drain_compression_jobs 收集结果。
调用图:调用 1 个内部函数(from_path);外部调用 9 个(elapsed, new, file, collect_next_compression_job, drain_compression_jobs, read_dir, try_exists, vec!, warn!)。
worker::CompressionOutcome::tag498–505 ↗
fn tag(self) -> &'static str
作用:把压缩结果转换成适合写入统计系统的短标签。比如“压缩成功”会变成 compressed。
数据流:输入是一个 CompressionOutcome 枚举值 → 匹配具体结果 → 输出固定字符串标签。
调用关系:collect_next_compression_job 收到任务结果后用它给指标打标签。
worker::CompressionMeasurement::new515–525 ↗
fn new(
outcome: CompressionOutcome,
source_bytes: Option<u64>,
compressed_bytes: Option<u64>,
) -> Self
作用:创建一条压缩测量结果,记录结果类型、原文件大小和压缩后大小。它是压缩任务交回给统计流程的小报告。
数据流:输入是 outcome、source_bytes、compressed_bytes → 放进 CompressionMeasurement 结构 → 输出测量对象。
调用关系:compress_rollout_if_cold_blocking 在各种结局下调用它,collect_next_compression_job 再读取它更新统计。
worker::drain_compression_jobs533–540 ↗
async fn drain_compression_jobs(
jobs: &mut JoinSet<CompressionJobResult>,
stats: &mut CompressionStats,
)
作用:把当前还没结束的压缩任务全部等完并收集结果。它用于目录扫描结束或中途需要清空队列的时候。
数据流:输入是任务集合和统计对象 → 只要任务集合不空,就反复调用 collect_next_compression_job → 最终任务集合被清空,统计被更新。
调用关系:compress_rollouts_in_root 在结束扫描或出错前调用它,实际处理单个任务结果交给 collect_next_compression_job。
调用图:外部调用 2 个(is_empty, collect_next_compression_job)。
worker::collect_next_compression_job542–586 ↗
async fn collect_next_compression_job(
jobs: &mut JoinSet<CompressionJobResult>,
stats: &mut CompressionStats,
)
作用:等待一个压缩任务完成,并把它的成功、跳过或失败反映到统计和监控里。
数据流:输入是任务集合和统计对象 → 取下一个完成的任务;成功则按 outcome 增加 compressed 或 skipped,并记录耗时、大小、压缩比;失败则增加 failed 并写警告 → 不返回数据,但更新统计和指标。
调用关系:drain_compression_jobs 和 compress_rollouts_in_root 都用它控制压缩任务队列;它调用 metrics 模块记录观测数据。
调用图:外部调用 7 个(join_next, compressed_bytes, compression_ratio, file, file_duration, source_bytes, warn!)。
worker::compress_rollout_if_cold_blocking588–657 ↗
fn compress_rollout_if_cold_blocking(path: &Path) -> io::Result<CompressionMeasurement>
作用:真正压缩一个 rollout 文件,但只处理“冷文件”,也就是至少 7 天没改过的文件。它非常小心,防止压缩期间文件被别人改动。
数据流:输入是普通 rollout 路径 → 检查是否够旧;若不够旧或已有压缩版就返回跳过;否则压缩到临时文件、验证能解压、确认原文件没变、复制时间和权限、原子式放到 .zst 路径,再次确认原文件没变,最后删除原文件 → 输出压缩测量结果或错误。
调用关系:compress_rollouts_in_root 把它放到阻塞线程中执行;它依赖 cold_file_state、encode_zstd_to_writer、verify_zstd、same_file_state 和 set_file_metadata。
调用图:外部调用 10 个(compressed_rollout_path, new, cold_file_state, encode_zstd_to_writer, same_file_state, set_file_metadata, verify_zstd, create_dir_all, remove_file, new)。
worker::cold_file_state665–689 ↗
fn cold_file_state(path: &Path) -> io::Result<ColdFileState>
作用:判断一个文件是否足够旧,可以安全考虑压缩。它同时记录文件大小、修改时间和权限,供后续确认文件没变。
数据流:输入是文件路径 → 读取文件元数据;不存在或不是普通文件就返回 NotCold;计算距离上次修改的时间,少于 7 天返回 NotCold,否则返回 Cold 和文件状态 → 输出冷热判断。
调用关系:compress_rollout_if_cold_blocking 开始时调用它,决定要压缩还是跳过。
worker::same_file_state691–699 ↗
fn same_file_state(path: &Path, expected: &FileState) -> io::Result<bool>
作用:检查文件现在是否和之前看到的一模一样。它用来判断压缩过程中有没有人改了文件。
数据流:输入是路径和期望的 FileState → 读取当前元数据,比较大小、修改时间和权限 → 输出 true 表示没变,false 表示已变或不存在,其他读错则返回错误。
调用关系:compress_rollout_if_cold_blocking 在写出压缩文件前后都调用它,避免删掉正在变化的源文件。
调用图:外部调用 1 个(metadata)。
worker::encode_zstd_to_writer701–707 ↗
worker::verify_zstd709–715 ↗
worker::set_file_metadata717–724 ↗
fn set_file_metadata(
file: &File,
modified: SystemTime,
permissions: &Permissions,
) -> io::Result<()>
作用:把压缩临时文件的修改时间和权限设置成和原文件一致。这样压缩后仍能保留原文件的重要外观信息。
数据流:输入是文件句柄、修改时间和权限 → 设置文件时间,再复制权限 → 返回成功或错误。
调用关系:compress_rollout_if_cold_blocking 在压缩并验证后调用它,再把临时文件落到正式位置。
调用图:外部调用 4 个(set_permissions, set_times, new, clone)。
worker::cleanup_stale_temps726–734 ↗
async fn cleanup_stale_temps(codex_home: &Path) -> io::Result<()>
作用:清理 sessions 相关目录里的过期临时文件。这样之前中断的压缩或解压不会一直留下垃圾文件。
数据流:输入是 codex_home → 分别拼出 sessions 和 archived sessions 根目录 → 对每个根目录调用 cleanup_stale_temps_in_root → 返回整体成功或错误。
调用关系:worker::run 每次正式压缩前调用它,具体递归扫描由 cleanup_stale_temps_in_root 完成。
调用图:外部调用 2 个(join, cleanup_stale_temps_in_root)。
worker::cleanup_stale_temps_in_root736–799 ↗
async fn cleanup_stale_temps_in_root(root: &Path) -> io::Result<()>
作用:在一个根目录下递归找出很久没动过的 .tmp 文件并删除。它只删足够旧的临时文件,避免误删正在使用的文件。
数据流:输入是根目录 → 如果目录不存在就结束;否则遍历所有子目录,找到文件名以 .tmp 结尾的普通文件,检查修改时间是否超过阈值 → 旧的就删除,并记录删除或失败指标。
调用关系:cleanup_stale_temps 调用它;它使用 temp_cleanup 指标记录清理结果,并在读目录或删文件失败时写警告。
调用图:外部调用 6 个(temp_cleanup, read_dir, remove_file, try_exists, vec!, warn!)。
metrics::file817–819 ↗
fn file(outcome: &'static str)
作用:记录一个文件级压缩结果,比如扫描、压缩、跳过或失败。它帮助运维知道每类结果发生了多少次。
数据流:输入是 outcome 标签 → 调用通用 counter 计数器加一 → 没有返回值。
调用关系:压缩扫描和任务收集流程会调用它;底层计数动作交给 metrics::counter。
调用图:外部调用 1 个(counter)。
metrics::file_duration821–823 ↗
fn file_duration(outcome: &'static str, duration: Duration)
作用:记录单个文件压缩或处理花了多久。这样可以发现压缩是否变慢。
数据流:输入是 outcome 标签和 Duration 时长 → 调用 duration_histogram 记录时长分布 → 不返回数据。
调用关系:collect_next_compression_job 在每个任务结束后调用它;实际上报交给 metrics::duration_histogram。
调用图:外部调用 1 个(duration_histogram)。
metrics::source_bytes825–831 ↗
fn source_bytes(outcome: &'static str, bytes: u64)
作用:记录原始文件大小。它用来观察被压缩的文件通常有多大。
数据流:输入是 outcome 和字节数 → 把字节数安全转换成 i64 → 写入直方图,也就是统计数值分布的指标 → 不返回数据。
调用关系:collect_next_compression_job 在测量结果里有原始大小时调用它;它依赖 saturating_i64 和 histogram。
调用图:外部调用 2 个(histogram, saturating_i64)。
metrics::compressed_bytes833–839 ↗
fn compressed_bytes(outcome: &'static str, bytes: u64)
作用:记录压缩后文件大小。它用来衡量压缩产物占了多少空间。
数据流:输入是 outcome 和压缩后字节数 → 安全转成 i64 → 写入压缩大小直方图 → 不返回数据。
调用关系:collect_next_compression_job 在压缩后大小存在时调用它;它把上报交给 histogram。
调用图:外部调用 2 个(histogram, saturating_i64)。
metrics::compression_ratio841–856 ↗
fn compression_ratio(
outcome: &'static str,
source_bytes: u64,
compressed_bytes: u64,
)
作用:记录压缩比例,也就是压缩后大小相对原大小的比例。它能告诉我们压缩是否划算。
数据流:输入是 outcome、原始字节数、压缩字节数 → 如果原始大小为 0 就跳过;否则按万分比算比例并安全转成 i64 → 写入比例直方图。
调用关系:collect_next_compression_job 在同时有原始大小和压缩大小时调用它;它使用 histogram 上报。
调用图:外部调用 3 个(histogram, saturating_i64, from)。
metrics::materialize858–860 ↗
fn materialize(outcome: &'static str)
作用:记录“把压缩文件解回普通文件”这件事的结果。比如本来就有普通文件、缺失、失败或成功解压。
数据流:输入是 outcome 标签 → 调用 counter 给 materialize 指标加一 → 不返回数据。
调用关系:materialize_rollout_for_append_blocking 在不同分支里调用它;底层由 metrics::counter 上报。
调用图:外部调用 1 个(counter)。
metrics::run862–864 ↗
fn run(status: &'static str)
作用:记录后台压缩整轮运行的状态,比如启动、完成、失败或跳过。它关注的是“一轮任务”,不是单个文件。
数据流:输入是 status 标签 → 调用 counter 给运行指标加一 → 不返回数据。
调用关系:worker::spawn 和 worker::run 用它记录压缩工人整体状态。
调用图:外部调用 1 个(counter)。
metrics::run_duration866–868 ↗
fn run_duration(status: &'static str, duration: Duration)
作用:记录一整轮后台压缩任务耗时。它帮助判断压缩任务是否拖得太久。
数据流:输入是 status 和时长 → 调用 duration_histogram 上报运行时长 → 不返回数据。
调用关系:worker::run 在成功或失败结束时调用它。
调用图:外部调用 1 个(duration_histogram)。
metrics::temp_cleanup870–872 ↗
fn temp_cleanup(outcome: &'static str)
作用:记录过期临时文件清理的结果。比如删掉了多少,失败了多少。
数据流:输入是 outcome 标签 → 调用 counter 给清理指标加一 → 不返回数据。
调用关系:cleanup_stale_temps_in_root 删除临时文件时调用它。
调用图:外部调用 1 个(counter)。
metrics::counter874–879 ↗
fn counter(name: &str, tags: &[(&str, &str)])
作用:通用计数器上报工具。它把“某件事发生一次”发给全局观测系统。
数据流:输入是指标名和标签 → 先取全局 metrics 对象;如果没有配置就什么也不做;有的话就计数加一 → 忽略上报错误。
调用关系:file、materialize、run、temp_cleanup 等函数都通过它统一上报计数。
调用图:外部调用 1 个(global)。
metrics::histogram881–886 ↗
fn histogram(name: &str, value: i64, tags: &[(&str, &str)])
作用:通用数值分布上报工具。它用来记录文件大小、压缩比例这类数值。
数据流:输入是指标名、数值和标签 → 获取全局 metrics;没有就跳过;有就记录直方图 → 忽略上报错误。
调用关系:source_bytes、compressed_bytes 和 compression_ratio 通过它上报数值指标。
调用图:外部调用 1 个(global)。
metrics::duration_histogram888–893 ↗
fn duration_histogram(name: &str, duration: Duration, tags: &[(&str, &str)])
作用:通用耗时上报工具。它专门记录 Duration 类型的时间长度。
数据流:输入是指标名、时长和标签 → 获取全局 metrics;没有就跳过;有就记录耗时 → 忽略上报错误。
调用关系:file_duration 和 run_duration 通过它上报耗时。
调用图:外部调用 1 个(global)。
metrics::saturating_i64895–897 ↗
fn saturating_i64(value: impl TryInto<i64>) -> i64
作用:把较大的数字安全转成 i64,转不下时用 i64 最大值代替。这样指标系统不会因为数字太大而报错。
数据流:输入是可尝试转换为 i64 的值 → 尝试转换 → 成功返回转换值,失败返回 i64::MAX。
调用关系:source_bytes、compressed_bytes 和 compression_ratio 在写直方图前用它做保护。
调用图:外部调用 1 个(try_into)。
existing_rollout_path902–904 ↗
async fn existing_rollout_path(path: &Path) -> Option<PathBuf>
作用:查找一个 rollout 当前真实存在的文件路径,并且优先选择普通 .jsonl。调用方不用自己猜文件有没有被压缩。
数据流:输入是路径 → 转交给 path 模块的 existing_rollout_path → 输出存在的普通或压缩路径,找不到则为空。
调用关系:这是对外公开的查找入口;内部规则由 path::existing_rollout_path 实现。
调用图:外部调用 1 个(existing_rollout_path)。
path::compressed_rollout_path913–923 ↗
fn compressed_rollout_path(path: &Path) -> PathBuf
作用:计算一个 rollout 的压缩文件路径。如果输入已经是压缩路径,就原样返回。
数据流:输入是路径 → 检查是不是 .jsonl.zst;不是的话在文件名后追加 .zst → 输出新路径。
调用关系:压缩、解压和存在性检查都会用这个路径规则。
调用图:外部调用 4 个(file_name, to_path_buf, with_file_name, is_compressed_rollout_path)。
path::plain_rollout_path925–933 ↗
fn plain_rollout_path(path: &Path) -> PathBuf
作用:计算一个 rollout 的普通文件路径。如果输入是 .jsonl.zst,就去掉 .zst。
数据流:输入是路径 → 读取文件名;如果末尾有压缩后缀就移除,否则原样返回 → 输出普通路径。
调用关系:materialize_rollout_for_append_blocking、path::existing_rollout_path 和 path::should_skip_compressed_sibling 会用它统一普通文件名。
调用图:外部调用 3 个(file_name, to_path_buf, with_file_name)。
path::is_compressed_rollout_path935–939 ↗
fn is_compressed_rollout_path(path: &Path) -> bool
作用:判断一个路径是不是 rollout 的压缩文件。规则是文件名必须以 .jsonl.zst 结尾。
数据流:输入是路径 → 取文件名并转成字符串 → 检查后缀 → 输出布尔值。
调用关系:RolloutFile::is_compressed、reader::open_once 和路径判断逻辑都会用它。
调用图:外部调用 1 个(file_name)。
path::should_skip_compressed_sibling941–943 ↗
fn should_skip_compressed_sibling(path: &Path) -> bool
作用:判断一个压缩文件是否应该在扫描时被忽略。若同名普通文件还在,就优先普通文件,跳过压缩兄弟。
数据流:输入是路径 → 先确认它是压缩 rollout,再算出普通路径并检查普通文件是否存在 → 输出是否跳过。
调用关系:RolloutFile::from_path 调用它,保证目录扫描不会把同一份 rollout 算两次。
调用图:调用 1 个内部函数(plain_rollout_path);外部调用 1 个(is_compressed_rollout_path)。
path::existing_rollout_path945–957 ↗
async fn existing_rollout_path(path: &Path) -> Option<PathBuf>
作用:在普通和压缩两种形态之间,找出当前磁盘上真正存在的 rollout 文件。它优先返回普通文件。
数据流:输入是路径 → 算出普通路径并检查是否存在普通文件;如果没有,再检查压缩路径 → 输出找到的路径或空。
调用关系:file_modified_time、reader::open_once 和对外 existing_rollout_path 都依赖它。
调用图:调用 2 个内部函数(compressed_rollout_path, plain_rollout_path);外部调用 1 个(matches!)。
file_name::parse_rollout_file_name963–970 ↗
fn parse_rollout_file_name(name: &str) -> Option<&str>
作用:检查文件名是否符合 rollout 命名:以 rollout- 开头、以 .jsonl 结尾。压缩后缀 .zst 会先被忽略。
数据流:输入是文件名字符串 → 去掉可选的 .zst → 检查前缀和后缀 → 合法则返回普通 .jsonl 名字,否则返回空。
调用关系:RolloutFile::from_path 和外层 parse_rollout_file_name 用它统一识别规则。
reader::open_once985–1007 ↗
async fn open_once(path: &Path) -> io::Result<RolloutLineReader>
作用:尝试打开一次 rollout 读者,不做重试。它会根据真实文件形态选择普通异步读,或压缩解码读。
数据流:输入是路径 → 先查实际存在的是普通还是压缩;若是压缩文件,就在阻塞线程里打开文件并创建 zstd 解码按行读者;若是普通文件,就异步打开并创建按行读者 → 输出 RolloutLineReader。
调用关系:open_rollout_line_reader 多次调用它来抵抗文件切换时的短暂不存在;它创建 RolloutLineReaderInner::Blocking 或 Plain。
调用图:外部调用 8 个(as_path, existing_rollout_path, is_compressed_rollout_path, Blocking, Plain, open, new, spawn_blocking)。
create_file_with_permissions1022–1029 ↗
fn create_file_with_permissions(path: &Path, permissions: &Permissions) -> io::Result<File>
作用:创建一个新文件,并尽量让它拥有指定权限。它用于解压时生成和原压缩文件权限一致的普通文件。
数据流:输入是目标路径和权限 → 以“必须新建,不能覆盖”的方式打开文件;Unix 系统会先用权限位创建,再设置权限;其他系统则创建后设置权限 → 输出文件句柄或错误。
调用关系:materialize_rollout_for_append_blocking 解压到临时文件时调用它,保证权限不丢。
temp_path_for1031–1042 ↗
fn temp_path_for(path: &Path, operation: &str) -> PathBuf
作用:为某个操作生成一个不容易撞名的临时文件路径。名字里包含操作名、进程号和递增计数。
数据流:输入是原路径和操作名 → 取原文件名,没有就用默认名;加上 .操作名.进程号.计数.tmp → 输出同目录下的新临时路径。
调用关系:materialize_rollout_for_append_blocking 调用它生成解压用临时文件名。
调用图:被 1 处调用(materialize_rollout_for_append_blocking);外部调用 3 个(file_name, with_file_name, format!)。
rollout/src/session_index.rs源码 ↗
这个文件解决的是“线程 ID 很难记,但名字好记”的问题。它把每次改名都追加写进一个 JSONL 文件(JSONL 就是一行一条 JSON 记录),像账本一样只往后加;查的时候通常从文件末尾往前看,因为最后一条才是最新名字。为了避免两个任务同时写坏这个账本,它用互斥锁(一把锁,防止两个任务同时改同一份数据)保护写入和删除。主要数据是 SessionIndexEntry,里面有线程 ID、线程名、更新时间。它既能按 ID 找最新名字,也能批量找很多 ID 的名字,还能按名字反查真实会话文件,并且会跳过那些只有索引、但实际会话文件还没保存好的记录。整体像一本通讯录:新增名字是加一行,查最新名字是从最后一页往前找。
append_thread_name33–50 ↗
async fn append_thread_name(
codex_home: &Path,
thread_id: ThreadId,
name: &str,
) -> std::io::Result<()>
作用:给某个线程记下一个新的名字,并自动带上当前 UTC 时间。用户重命名会话时会用它,让以后能按这个名字找到会话。
数据流:进去的是项目主目录 codex_home、线程 ID、名字字符串 → 它取当前时间,组装成一条 SessionIndexEntry 记录 → 交给 append_session_index_entry 写进索引文件,成功后没有额外返回,只表示写入完成。
调用关系:它是更方便的上层入口:调用者只需要给线程 ID 和名字,不用自己准备时间和 JSON。它内部先取时间,再把真正写文件的活儿交给 append_session_index_entry。
调用图:调用 1 个内部函数(append_session_index_entry);外部调用 1 个(now_utc)。
append_session_index_entry54–71 ↗
async fn append_session_index_entry(
codex_home: &Path,
entry: &SessionIndexEntry,
) -> std::io::Result<()>
作用:把一条已经准备好的会话索引记录追加写到 session_index.jsonl。它是实际落盘的写入点,保证名字更新被保存下来。
数据流:进去的是主目录和一条索引记录 → 它先拿互斥锁,算出索引文件路径,把记录转成一行 JSON,并在末尾加换行 → 追加写入文件并刷新到磁盘;文件可能会被自动创建。
调用关系:它被 append_thread_name 调用,是写索引账本的底层函数。它会调用 session_index_path 找到文件位置,也会用 JSON 序列化把结构体变成可存储的文本。
调用图:调用 1 个内部函数(session_index_path);被 1 处调用(append_thread_name);外部调用 2 个(to_string, new)。
remove_thread_name_entries74–105 ↗
async fn remove_thread_name_entries(
codex_home: &Path,
thread_id: ThreadId,
) -> std::io::Result<()>
作用:删除某个线程在索引文件里留下的所有名字记录。比如会话被删除或需要彻底清理名字时,就用它把旧账擦掉。
数据流:进去的是主目录和线程 ID → 它加锁后读取整个索引文件;如果文件不存在,就直接算成功 → 它逐行解析 JSON,过滤掉这个线程 ID 的记录,把剩下的内容写到临时文件,再用重命名替换原文件。
调用关系:它和追加写入一样会先通过 session_index_path 找到账本文件,也用同一把锁避免边删边写。它不调用查找函数,而是直接重写文件,因为删除需要清掉所有历史记录。
调用图:调用 1 个内部函数(session_index_path);外部调用 4 个(with_capacity, read_to_string, rename, write)。
find_thread_name_by_id108–121 ↗
async fn find_thread_name_by_id(
codex_home: &Path,
thread_id: &ThreadId,
) -> std::io::Result<Option<String>>
作用:按线程 ID 找这个线程最新的名字。列表展示或恢复会话标题时,可以用它把难看的 ID 变成用户看得懂的名字。
数据流:进去的是主目录和线程 ID → 它先找到索引文件;如果文件不存在就返回“没有名字” → 它把从文件末尾扫描的工作放到阻塞线程里做,找到最新匹配记录后取出 thread_name 返回。
调用关系:它是按单个 ID 查名字的入口。为了不堵住异步运行环境,它用 spawn_blocking 把普通磁盘扫描放到专门线程里;实际扫描逻辑由从末尾查找的辅助函数完成。
调用图:调用 1 个内部函数(session_index_path);外部调用 1 个(spawn_blocking)。
find_thread_names_by_ids124–153 ↗
async fn find_thread_names_by_ids(
codex_home: &Path,
thread_ids: &HashSet<ThreadId>,
) -> std::io::Result<HashMap<ThreadId, String>>
作用:一次性给一批线程 ID 找名字。会话列表很多时,用它比一个一个查更合适。
数据流:进去的是主目录和一组线程 ID → 如果 ID 集合为空或索引文件不存在,就返回空表 → 它异步打开文件,从头逐行读 JSON;只要这一行的 ID 在目标集合里、名字也不为空,就放进结果表。后出现的同 ID 记录会覆盖先前记录,所以最后留下的是最新名字。
调用关系:它会被 filter_thread_items_by_search_term 使用,帮助搜索或过滤会话列表时拿到名字。它自己只调用 session_index_path 找文件,然后直接流式读取文件内容。
调用图:调用 1 个内部函数(session_index_path);被 1 处调用(filter_thread_items_by_search_term);外部调用 4 个(new, with_capacity, open, new)。
find_thread_meta_by_name_str157–195 ↗
async fn find_thread_meta_by_name_str(
codex_home: &Path,
name: &str,
state_db_ctx: Option<&codex_state::StateRuntime>,
) -> std::io::Result<Option<(PathBuf, SessionMetaLine)>>
作用:按用户输入的名字找到对应的真实会话文件,并读出这个会话的元信息。它不只信索引,还会确认实际会话文件能读,避免打开一个只在索引里存在的“空壳”。
数据流:进去的是主目录、名字、可选的状态数据库上下文 → 名字为空或索引不存在时直接返回没有结果 → 它从索引末尾开始把匹配这个名字的线程 ID 一个个送出来;每拿到一个 ID,就去找真实会话路径并读取会话头部元信息 → 找到第一个可读的就返回路径和元信息,否则返回没有结果。
调用关系:它把索引查找和真实会话读取串起来。扫描索引的耗时工作放进 spawn_blocking,线程 ID 通过通道传回;随后它调用 find_thread_path_by_id_str 找文件,再调用 read_session_meta_line 读取元信息。这样最新但未保存完整的重命名不会挡住旧的可用会话。
调用图:调用 3 个内部函数(find_thread_path_by_id_str, read_session_meta_line, session_index_path);外部调用 2 个(channel, spawn_blocking)。
session_index_path197–199 ↗
fn session_index_path(codex_home: &Path) -> PathBuf
作用:拼出会话索引文件的完整路径。所有读写索引的人都用它,避免大家各写各的路径字符串。
数据流:进去的是 Codex 主目录路径 → 它在这个目录后面接上固定文件名 session_index.jsonl → 出来的是完整文件路径。
调用关系:它是全文件共享的小工具,被追加、删除、按 ID 查、批量查、按名字查这些函数调用。它本身不读写磁盘,只负责统一文件位置。
调用图:被 5 处调用(append_session_index_entry, find_thread_meta_by_name_str, find_thread_name_by_id, find_thread_names_by_ids, remove_thread_name_entries);外部调用 1 个(join)。
scan_index_from_end_by_id201–206 ↗
fn scan_index_from_end_by_id(
path: &Path,
thread_id: &ThreadId,
) -> std::io::Result<Option<SessionIndexEntry>>
作用:从索引文件末尾往前找某个线程 ID 的最新记录。因为新记录总是追加到最后,所以这样能最快找到最新名字。
数据流:进去的是索引文件路径和线程 ID → 它设置一个判断条件:记录里的 ID 必须等于目标 ID → 把这个条件交给通用的末尾扫描函数,返回找到的第一条匹配记录,或者没有结果。
调用关系:它是按 ID 反向扫描的专用包装。自己不处理文件块和行解析,而是把工作交给 scan_index_from_end。
调用图:调用 1 个内部函数(scan_index_from_end)。
stream_thread_ids_from_end_by_name208–224 ↗
fn stream_thread_ids_from_end_by_name(
path: &Path,
name: &str,
tx: tokio::sync::mpsc::Sender<ThreadId>,
) -> std::io::Result<()>
作用:从索引末尾开始,按名字找可能对应的线程 ID,并一个个发给外部使用。它特别注意同一个线程改过名的情况,只看每个线程最新那条名字。
数据流:进去的是索引路径、目标名字、一个发送通道 → 它维护一个“见过的线程 ID”集合,从后往前扫描每条记录;如果这是某个 ID 的最新记录,并且名字正好匹配,就把这个 ID 通过通道发出去 → 扫完或接收方关闭通道后结束。
调用关系:它服务于 find_thread_meta_by_name_str:后者一边接收这里送出的 ID,一边检查真实会话文件是否可读。它把逐条扫描的细节交给 scan_index_from_end_for_each。
调用图:调用 1 个内部函数(scan_index_from_end_for_each);外部调用 1 个(new)。
scan_index_from_end226–239 ↗
fn scan_index_from_end(
path: &Path,
mut predicate: F,
) -> std::io::Result<Option<SessionIndexEntry>>
作用:提供一个通用的“从文件末尾往前找第一条满足条件的索引记录”的能力。不同查询只要给它不同的判断条件即可。
数据流:进去的是索引文件路径和一个判断函数 → 它把判断函数包装成“看到一条记录就决定要不要返回”的访问函数 → 交给更底层的扫描器,最后返回第一条满足条件的记录或没有结果。
调用关系:它被 scan_index_from_end_by_id 调用,是介于具体查询和底层文件扫描之间的一层。真正按块读文件、拼行、解析 JSON 的活儿由 scan_index_from_end_for_each 做。
调用图:调用 1 个内部函数(scan_index_from_end_for_each);被 1 处调用(scan_index_from_end_by_id)。
scan_index_from_end_for_each241–276 ↗
fn scan_index_from_end_for_each(
path: &Path,
mut visit_entry: F,
) -> std::io::Result<Option<SessionIndexEntry>>
作用:这是反向扫描索引文件的核心机器。它不用把整个文件一次性读进内存,而是一块一块从后往前读,适合索引文件变大时仍然查得动。
数据流:进去的是索引文件路径和一个“每看到一条记录要做什么”的函数 → 它打开文件,从文件尾部按固定大小读取字节块,倒着找换行符,把一行内容拼出来并交给 parse_line_from_rev 解析 → 如果访问函数要求停止,就返回那条记录;否则扫完整个文件后返回没有结果。
调用关系:它是所有反向查找的底层引擎,被 scan_index_from_end 和 stream_thread_ids_from_end_by_name 使用。它自己负责文件定位和分块读取,但把单行解析交给 parse_line_from_rev。
调用图:调用 1 个内部函数(parse_line_from_rev);被 2 处调用(scan_index_from_end, stream_thread_ids_from_end_by_name);外部调用 5 个(open, Start, new, try_from, vec!)。
parse_line_from_rev278–304 ↗
fn parse_line_from_rev(
line_rev: &mut Vec<u8>,
visit_entry: &mut F,
) -> std::io::Result<Option<SessionIndexEntry>>
作用:把反向扫描时临时攒出来的一行字节,恢复成正常顺序,并尝试解析成一条索引记录。坏行、空行、乱码行都会被安静跳过。
数据流:进去的是一段倒着存的行内容和一个访问函数 → 它先把字节顺序翻回来,转成 UTF-8 字符串,去掉 Windows 风格换行里的回车符,再修剪空白并解析 JSON → 如果解析成功,就把记录交给访问函数,返回访问函数给出的结果。
调用关系:它只被 scan_index_from_end_for_each 调用,是底层扫描器的“单行清洗和解析”零件。这样扫描器不用关心 JSON 细节,只负责把行切出来。
调用图:被 1 处调用(scan_index_from_end_for_each);外部调用 2 个(from_utf8, take)。
rollout/src/search.rs源码 ↗
会话记录像一本本流水账,通常是 .jsonl 文件,也可能被压缩保存。用户想按关键词找旧对话时,不能只搜文件名,也不能漏掉压缩文件。这个文件就是搜索入口:先选普通会话目录或归档会话目录,再把用户输入转成适合在 JSON 文本里查找的样子。如果系统里有 ripgrep(一个很快的命令行文本搜索工具),它先用 ripgrep 快速找普通 .jsonl 文件;如果没有,就自己一层层翻目录、逐行读文件。压缩文件不能直接交给 ripgrep,所以它会用项目里的解压读取器打开,再从聊天消息里提取真正给人看的文字,截出一小段上下文作为预览。结果里用“原始 .jsonl 路径”当钥匙,普通匹配没有预览,压缩匹配通常带一段命中片段。
search_rollout_paths27–39 ↗
async fn search_rollout_paths(
rg_command: &Path,
codex_home: &Path,
archived: bool,
search_term: &str,
) -> io::Result<HashSet<PathBuf>>
作用:只关心“哪些会话文件命中了”时用这个函数。它把更详细的搜索结果简化成一组文件路径。
数据流:输入 ripgrep 程序位置、Codex 主目录、是否搜归档、关键词 → 调用完整搜索拿到“路径到预览片段”的表 → 丢掉预览,只返回路径集合,不直接改动文件。
调用关系:它是给外层代码的简化接口。真正的搜索活儿交给 search_rollout_matches,自己只负责把结果从详细版变成路径版。
调用图:调用 1 个内部函数(search_rollout_matches)。
search_rollout_matches41–62 ↗
async fn search_rollout_matches(
rg_command: &Path,
codex_home: &Path,
archived: bool,
search_term: &str,
) -> io::Result<RolloutSearchMatches>
作用:这是本文件最主要的搜索入口。它返回命中的会话文件,并在能做到时附上一段命中内容预览。
数据流:输入 ripgrep 路径、Codex 主目录、是否归档、搜索词 → 先确定要搜 sessions 还是 archived sessions 目录,再把搜索词转成 JSON 文件里的转义写法 → 优先用 ripgrep 搜普通 .jsonl,如果 ripgrep 不可用就改为手动扫描 → 另外扫描压缩记录并合并结果 → 输出路径到可选预览文本的映射。
调用关系:search_rollout_paths 会调用它。它像调度员:让 json_escaped_search_term 准备搜索词,让 ripgrep_rollout_paths 尝试快搜,必要时让 scan_rollout_matches 全目录慢搜,再让 scan_compressed_rollout_matches 补上压缩文件。
调用图:调用 4 个内部函数(json_escaped_search_term, ripgrep_rollout_paths, scan_compressed_rollout_matches, scan_rollout_matches);被 1 处调用(search_rollout_paths);外部调用 1 个(join)。
ripgrep_rollout_paths64–115 ↗
async fn ripgrep_rollout_paths(
rg_command: &Path,
root: &Path,
search_term: &str,
) -> io::Result<Option<HashSet<PathBuf>>>
作用:用 ripgrep 快速找普通 .jsonl 会话文件。ripgrep 是专门搜文本的外部程序,速度通常比自己逐个文件读快很多。
数据流:输入 ripgrep 命令路径、要搜索的根目录、已经适配 JSON 文本的关键词 → 如果目录不存在,直接返回空集合 → 启动 ripgrep,只查 .jsonl,忽略大小写,按固定字符串搜索 → 把命令输出的每一行路径整理成绝对路径集合 → 如果找不到 ripgrep 程序,返回 None 表示需要换备用方案。
调用关系:search_rollout_matches 先找它试试快路。它不处理压缩文件,也不生成预览;成功时只把命中的普通文件路径交回去。
调用图:被 1 处调用(search_rollout_matches);外部调用 8 个(new, join, from, from_utf8_lossy, new, other, format!, try_exists)。
scan_rollout_matches117–163 ↗
async fn scan_rollout_matches(
root: &Path,
json_search_term: &str,
search_term: &str,
) -> io::Result<RolloutSearchMatches>
作用:当不能依赖 ripgrep,或需要自己处理各种记录文件时,用这个函数手动翻目录搜索。它能同时处理普通和压缩的会话文件。
数据流:输入搜索根目录、JSON 形式关键词、原始关键词 → 把 JSON 关键词做成忽略大小写的正则表达式(可理解为更灵活的匹配器)→ 从根目录开始递归查看每个文件 → 普通记录逐行查是否包含关键词,压缩记录则打开内容并尝试截取预览 → 输出命中文件到预览的映射。
调用关系:search_rollout_matches 在 ripgrep 不可用时调用它。它把普通文件的检查交给 rollout_contains,把压缩文件的预览提取交给 first_rollout_content_match_snippet。
调用图:调用 4 个内部函数(from_path, case_insensitive_literal_regex, first_rollout_content_match_snippet, rollout_contains);被 1 处调用(search_rollout_matches);外部调用 4 个(new, plain_rollout_path, read_dir, vec!)。
rollout_contains165–173 ↗
async fn rollout_contains(path: &Path, search_term: &Regex) -> io::Result<bool>
作用:检查一个会话记录文件里是否出现过关键词。它只回答“有没有”,不负责给出命中片段。
数据流:输入文件路径和已经准备好的匹配器 → 用 compression::open_rollout_line_reader 打开文件,逐行读取 → 只要有一行匹配就立刻返回 true → 如果读完都没有匹配,返回 false。
调用关系:scan_rollout_matches 在处理普通会话文件时调用它。它是一个省力的早停检查器,找到第一处就不用继续读完整个文件。
调用图:调用 1 个内部函数(open_rollout_line_reader);被 1 处调用(scan_rollout_matches);外部调用 1 个(is_match)。
first_rollout_content_match_snippet175–190 ↗
async fn first_rollout_content_match_snippet(
path: &Path,
search_term: &str,
) -> io::Result<Option<String>>
作用:在一个会话记录里找第一段真正聊天内容的命中预览。它适合压缩文件,因为压缩文件没法直接让 ripgrep 给出好看的结果。
数据流:输入文件路径和用户搜索词 → 打开记录逐行读 → 同时准备两种忽略大小写的匹配器:一种用于判断 JSON 行里有没有这个词,一种用于在还原出的聊天文字里定位 → 找到第一条能提取出内容片段的记录就返回这段 snippet → 找不到则返回 None。
调用关系:scan_rollout_matches 和 scan_compressed_rollout_matches 都会调用它来处理压缩记录。它内部先准备 json_escaped_search_term 和 case_insensitive_literal_regex,再把具体一行的解析交给 content_match_snippet。
调用图:调用 4 个内部函数(open_rollout_line_reader, case_insensitive_literal_regex, content_match_snippet, json_escaped_search_term);被 2 处调用(scan_compressed_rollout_matches, scan_rollout_matches)。
scan_compressed_rollout_matches192–233 ↗
async fn scan_compressed_rollout_matches(
root: &Path,
search_term: &str,
) -> io::Result<RolloutSearchMatches>
作用:专门补搜压缩过的会话记录。因为外部文本搜索工具通常不能直接看懂压缩文件里的内容,所以这里必须自己打开检查。
数据流:输入会话根目录和搜索词 → 递归遍历目录 → 只保留项目认识的压缩 rollout 文件 → 对每个压缩文件找第一段命中预览 → 输出普通 .jsonl 形式路径到预览片段的映射。
调用关系:search_rollout_matches 在 ripgrep 搜完普通文件后调用它,防止压缩历史被漏掉。它把单个文件里的查找交给 first_rollout_content_match_snippet。
调用图:调用 2 个内部函数(from_path, first_rollout_content_match_snippet);被 1 处调用(search_rollout_matches);外部调用 4 个(new, plain_rollout_path, read_dir, vec!)。
json_escaped_search_term235–238 ↗
fn json_escaped_search_term(search_term: &str) -> io::Result<String>
作用:把用户输入改写成它在 JSON 文件里实际出现的样子。比如引号、换行这类字符,在 JSON 里会被转义,直接搜原文可能搜不到。
数据流:输入原始搜索词 → 用 JSON 序列化把特殊字符转成 JSON 写法 → 去掉最外层多出来的双引号 → 输出可用于搜索 JSON 行内容的字符串。
调用关系:search_rollout_matches 用它准备给 ripgrep 或手动扫描的关键词;first_rollout_content_match_snippet 也用它先判断一行 JSON 里是否可能包含目标词。
调用图:被 2 处调用(first_rollout_content_match_snippet, search_rollout_matches);外部调用 1 个(to_string)。
case_insensitive_literal_regex240–245 ↗
fn case_insensitive_literal_regex(search_term: impl AsRef<str>) -> io::Result<Regex>
作用:把一段普通文字做成忽略大小写的匹配器。这里会把文字按“字面意思”处理,不让用户输入里的符号被误当成正则规则。
数据流:输入任意可转成字符串的搜索词 → 先转义其中的特殊符号 → 再创建忽略大小写的 Regex(正则表达式,也就是文本匹配器)→ 输出可反复使用的匹配器,创建失败则返回错误。
调用关系:scan_rollout_matches 用它检查普通文件行;first_rollout_content_match_snippet 用它同时准备 JSON 行匹配和聊天正文匹配。
调用图:被 2 处调用(first_rollout_content_match_snippet, scan_rollout_matches);外部调用 3 个(as_ref, new, escape)。
content_match_snippet247–251 ↗
fn content_match_snippet(jsonl_line: &str, search_term: &Regex) -> Option<String>
作用:从一行会话 JSON 里解析出聊天文字,并尝试截出命中关键词附近的一小段。它把机器保存格式变成给人看的预览。
数据流:输入一整行 JSONL 文本和搜索匹配器 → 先把 JSON 解析成 RolloutLine → 从里面拿出用户或助手真正说的话 → 在文字里找到关键词,并截取前后上下文 → 输出预览片段;解析失败、不是聊天内容或没命中时输出 None。
调用关系:first_rollout_content_match_snippet 逐行读到可能命中的记录后调用它。它把“取出聊天文字”交给 conversation_text_from_item,把“截上下文”交给 excerpt_around_match。
调用图:调用 2 个内部函数(conversation_text_from_item, excerpt_around_match);被 1 处调用(first_rollout_content_match_snippet)。
conversation_text_from_item253–289 ↗
fn conversation_text_from_item(item: &RolloutItem) -> Option<String>
作用:从一条 rollout 记录里挑出真正适合搜索展示的对话文字。它会跳过元数据、上下文、图片等不该当作聊天预览的内容。
数据流:输入一个 RolloutItem → 如果是用户消息,就去掉系统加在前面的用户消息标记并清理空白 → 如果是助手消息,就取非空文本 → 如果是响应消息,就只接受 user 或 assistant 角色,并拼接其中的文字内容 → 其他类型返回 None。
调用关系:content_match_snippet 需要先靠它把复杂记录变成普通文字。它会调用 strip_user_message_prefix 清掉用户消息前缀,并配合 content_item_text 从消息内容块里取文本。
调用图:调用 1 个内部函数(strip_user_message_prefix);被 1 处调用(content_match_snippet)。
content_item_text291–296 ↗
fn content_item_text(item: &ContentItem) -> Option<&str>
作用:从一个消息内容块里取出文字。它会接受输入文本和输出文本,但忽略图片,因为图片本身不能直接拿来做文字预览。
数据流:输入一个 ContentItem → 如果是 InputText 或 OutputText,就返回里面的 text 字符串引用 → 如果是 InputImage,就返回 None → 不创建新文本,也不改动原数据。
调用关系:它是 conversation_text_from_item 拼接响应消息内容时用的小筛子:只让文字内容通过,把图片内容挡掉。
strip_user_message_prefix298–303 ↗
fn strip_user_message_prefix(text: &str) -> &str
作用:去掉用户消息里项目内部加的开头标记,让预览看起来像人真正输入的话。没有这个处理,搜索结果可能会显示一段对用户没意义的前缀。
数据流:输入用户消息字符串 → 查找 USER_MESSAGE_BEGIN 这个内部标记 → 找到就返回标记后面的文字并去掉两端空白;找不到就直接返回去掉空白后的原文。
调用关系:conversation_text_from_item 在处理用户消息时调用它。它只做清理,不参与文件读取或搜索。
调用图:被 1 处调用(conversation_text_from_item)。
excerpt_around_match305–327 ↗
fn excerpt_around_match(text: &str, search_term: &Regex) -> Option<String>
作用:把一大段聊天文字缩成关键词附近的一小段预览。这样搜索结果不用显示整条长消息,也能让人知道命中发生在什么上下文里。
数据流:输入原文和搜索匹配器 → 先把换行、多余空格压成普通空格 → 找到关键词位置 → 向前取固定数量字符、向后取固定数量字符 → 如果截掉了开头或结尾,就加上省略号 → 输出最终片段,没命中或片段为空则返回 None。
调用关系:content_match_snippet 在拿到聊天正文后调用它。它把文本清理交给 normalize_preview_text,把按字符安全截取的边界计算交给 char_start_before 和 char_end_after。
调用图:调用 3 个内部函数(char_end_after, char_start_before, normalize_preview_text);被 1 处调用(content_match_snippet);外部调用 2 个(find, new)。
normalize_preview_text329–331 ↗
fn normalize_preview_text(text: &str) -> String
作用:把预览文字里的换行、制表符和连续空格压成单个空格。这样搜索结果不会乱换行,也更像一句正常的话。
数据流:输入任意文本 → 按空白切成一段段词 → 再用单个空格拼回去 → 输出规整后的字符串。
调用关系:excerpt_around_match 在截取预览前调用它。它保证后面的字符位置计算和最终展示都更稳定。
调用图:被 1 处调用(excerpt_around_match)。
char_start_before333–340 ↗
fn char_start_before(text: &str, byte_index: usize, chars_before: usize) -> usize
作用:计算预览片段应该从哪里开始,确保按“字符”往前数,而不是按字节硬切。这样不会把中文或 emoji 这类多字节字符切坏。
数据流:输入文本、命中位置的字节下标、希望往前保留的字符数 → 从命中点之前倒着数指定数量的字符 → 输出安全的起始字节下标;如果前面不够长,就返回 0。
调用关系:excerpt_around_match 用它决定片段左边界。它只负责算位置,不关心搜索词是什么,也不读取文件。
调用图:被 1 处调用(excerpt_around_match)。
char_end_after342–348 ↗
fn char_end_after(text: &str, byte_index: usize, chars_after: usize) -> usize
作用:计算预览片段应该到哪里结束,同样避免把中文或 emoji 之类的字符从中间切开。
数据流:输入文本、命中结束位置的字节下标、希望往后保留的字符数 → 从命中点之后正着数指定数量的字符 → 输出安全的结束字节下标;如果后面不够长,就用文本末尾。
调用关系:excerpt_around_match 用它决定片段右边界。它和 char_start_before 配成一对,保证预览截取既短又不破坏字符。
调用图:被 1 处调用(excerpt_around_match)。
Rollout 记录与发现
这些文件实现主要的 rollout 工作流,用于写入会话转录、重新加载它们,以及列出或定位磁盘上持久保存的线程。
rollout/src/list.rs源码 ↗
项目会把每次对话保存成一个 rollout 文件,可以理解成一本本流水账。这个文件就是“图书管理员”:它知道这些文件放在哪些目录里,文件名里藏着创建时间和唯一 ID,文件内容开头又藏着工作目录、模型、来源、第一条用户消息等信息。用户打开历史列表时,它会扫描这些文件,按创建时间或更新时间排好,挑出一页返回,并给出下一页用的游标(一个继续往后翻的时间标记)。它还会限制最多扫描多少文件,避免一次请求把硬盘翻个底朝天。除此之外,它能读单个文件的摘要,也能用数据库、文件名扫描和全文搜索几种办法,按 UUID 找到某个线程的文件。没有它,历史会话列表就无法稳定分页、筛选,也很难从一个线程 ID 找回原始记录。
Cursor::new147–149 ↗
fn new(ts: OffsetDateTime) -> Self
作用:创建一个分页游标。游标就是“上次看到哪儿了”的书签,用一个时间点表示。
数据流:进去一个时间戳 → 它把这个时间戳包进 Cursor 结构里 → 出来一个可以保存、传回前端、下次继续翻页用的游标。
调用关系:它是游标的基础构造函数,解析游标、生成下一页游标、从旧状态转换游标时都会用它。列表流程靠它把时间点变成统一的分页标记。
调用图:被 31 处调用(run_sse, prepare_encoded_json, into_prepared_stores_compressed_body_for_reuse, unpack_plugin_bundle_tar_gz, extract_zipball_to_dir, curated_repo_backup_archive_zip_bytes, curated_repo_zipball_bytes, extract_zip_to_dir, original_detail_images_are_capped_at_max_patch_count, original_detail_images_scale_with_dimensions (+15 more))。
Cursor::timestamp151–153 ↗
fn timestamp(&self) -> OffsetDateTime
作用:取出游标里保存的时间点。有人需要知道这个书签具体指向哪个时间时会用它。
数据流:进去一个 Cursor → 它读取里面的 ts 字段 → 返回这个游标记录的时间戳,不改动任何东西。
调用关系:它是给同一模块内其他代码查看游标时间的便捷入口,通常用于把分页位置交给别的状态或查询逻辑。
AnchorState::new166–177 ↗
fn new(anchor: Option<Cursor>) -> Self
作用:根据上一页的游标,准备一个“跳过已看内容”的状态。没有游标时,就表示从最新内容开始看。
数据流:进去一个可有可无的 Cursor → 如果有,就记住它的时间并标记为还没越过;如果没有,就从最早时间开始并允许立即收集 → 出来一个 AnchorState。
调用关系:按创建时间、更新时间、扁平目录、日期目录几种遍历方式开始时都会先建它。之后每遇到一个文件,都交给 AnchorState::should_skip 判断要不要跳过。
调用图:被 4 处调用(traverse_directories_for_paths_created, traverse_directories_for_paths_updated, traverse_flat_paths_created, traverse_flat_paths_updated)。
AnchorState::should_skip179–189 ↗
fn should_skip(&mut self, ts: OffsetDateTime, _id: Uuid) -> bool
作用:判断当前文件是不是还在上一页之前,是否应该先跳过。它保证翻页时不会重复返回已经看过的文件。
数据流:进去当前文件的时间戳和 ID → 如果还没越过上一页时间,就继续跳过;一旦遇到更旧的时间,就打开开关开始收集 → 返回 true 表示跳过,false 表示可以处理。
调用关系:FilesByCreatedAtVisitor::visit 和各个遍历函数在处理文件前会问它一句。它是分页稳定性的核心小零件。
调用图:被 1 处调用(visit)。
FilesByCreatedAtVisitor::visit220–254 ↗
async fn visit(
&mut self,
ts: OffsetDateTime,
id: Uuid,
path: PathBuf,
scanned: usize,
) -> ControlFlow<()>
作用:在按创建时间扫描文件时,处理一个候选 rollout 文件。它决定是否跳过、是否停止、以及是否把这个文件变成列表项。
数据流:进去文件的创建时间、UUID、路径和已扫描数量 → 它先看扫描上限和分页游标,再读文件更新时间,最后调用 build_thread_item 抽取摘要并应用筛选 → 可能往结果列表里加入一个 ThreadItem,也可能提前停止扫描。
调用关系:walk_rollout_files 每发现一个文件就调用它。它把底层目录扫描和上层“我要一页线程列表”的需求接起来。
调用图:调用 3 个内部函数(should_skip, build_thread_item, file_modified_time);外部调用 2 个(Break, Continue)。
FilesByUpdatedAtVisitor::visit264–278 ↗
async fn visit(
&mut self,
_ts: OffsetDateTime,
id: Uuid,
path: PathBuf,
_scanned: usize,
) -> ControlFlow<()>
作用:在按更新时间排序时,先轻量收集文件候选项。因为更新时间不在文件名里,必须先读文件属性再统一排序。
数据流:进去文件路径和 UUID → 它读取文件修改时间 → 把路径、ID、更新时间放进候选列表,不马上生成完整摘要。
调用关系:collect_files_by_updated_at 通过 walk_rollout_files 调用它。后续 traverse_directories_for_paths_updated 会把这些候选项排序,再逐个构造成 ThreadItem。
调用图:调用 1 个内部函数(file_modified_time);外部调用 1 个(Continue)。
Cursor::serialize282–291 ↗
fn serialize(&self, serializer: S) -> Result<S::Ok, S::Error>
作用:把游标变成可以传输或保存的字符串。这里使用 RFC3339 时间格式,也就是常见的标准时间写法。
数据流:进去一个 Cursor → 它把内部时间格式化成标准时间字符串 → 输出给序列化器,最终变成 JSON 里的字符串。
调用关系:当接口返回 next_cursor 时会用到它。这样调用方不需要理解内部结构,只要保存这个字符串下次传回来即可。
调用图:外部调用 2 个(format, serialize_str)。
Cursor::deserialize295–301 ↗
fn deserialize(deserializer: D) -> Result<Self, D::Error>
作用:把外部传回来的游标字符串变回 Cursor。它负责拒绝不合法的游标。
数据流:进去 JSON 字符串 → 它交给 parse_cursor 解析 → 成功就得到 Cursor,失败就返回“invalid cursor”错误。
调用关系:分页请求进入系统时会经过它。它和 Cursor::serialize 配成一对,一个负责发出去,一个负责收回来。
调用图:调用 1 个内部函数(parse_cursor);外部调用 1 个(deserialize)。
Cursor::from305–312 ↗
fn from(anchor: codex_state::Anchor) -> Self
作用:把 codex_state 里的 Anchor 转成这里使用的 Cursor。Anchor 可以理解成另一个模块保存的分页锚点。
数据流:进去一个 Anchor → 它把其中的纳秒时间转成 OffsetDateTime,失败时退回 Unix 起点时间 → 输出 Cursor。
调用关系:这是状态模块和列表模块之间的适配器,让旧的或外部保存的锚点能继续用于这里的分页逻辑。
调用图:外部调用 1 个(new)。
get_threads319–344 ↗
async fn get_threads(
codex_home: &Path,
page_size: usize,
cursor: Option<&Cursor>,
sort_key: ThreadSortKey,
allowed_sources: &[SessionSource],
model_providers: Option<&[String
作用:这是最常用的入口:从 codex_home 下的默认 sessions 目录读取线程列表。调用者不用自己拼目录路径。
数据流:进去 codex_home、页大小、游标、排序方式、来源筛选、模型筛选、工作目录筛选和默认模型提供方 → 它拼出 sessions 根目录并组装配置 → 调用 get_threads_in_root 返回 ThreadsPage。
调用关系:历史列表、测试和一些查找最新线程的逻辑会调用它。它把外部请求转成更通用的 get_threads_in_root 调用。
调用图:调用 1 个内部函数(get_threads_in_root);被 15 处调用(find_latest_thread_path, list_threads_from_files_desc_unfiltered, test_base_instructions_missing_in_meta_defaults_to_null, test_base_instructions_present_in_meta_is_preserved, test_created_at_sort_uses_file_mtime_for_updated_at, test_get_thread_contents, test_goal_first_thread_reads_later_user_message, test_list_conversations_latest_first, test_list_threads_scans_past_head_for_user_event, test_list_threads_uses_goal_objective_as_preview (+5 more));外部调用 1 个(join)。
get_threads_in_root346–395 ↗
async fn get_threads_in_root(
root: PathBuf,
page_size: usize,
cursor: Option<&Cursor>,
sort_key: ThreadSortKey,
config: ThreadListConfig<'_>,
) -> io::Result<ThreadsPage>
作用:从指定根目录读取线程列表。它比 get_threads 更底层,允许调用者指定目录布局和筛选配置。
数据流:进去根目录、页大小、游标、排序方式和 ThreadListConfig → 如果目录不存在就返回空页;否则准备 ProviderMatcher,并按目录布局选择日期嵌套扫描或扁平扫描 → 返回一页线程结果。
调用关系:get_threads 会把默认 sessions 目录交给它。它再分发给 traverse_directories_for_paths 或 traverse_flat_paths,是列表流程的调度中心。
调用图:调用 2 个内部函数(traverse_directories_for_paths, traverse_flat_paths);被 2 处调用(get_threads, list_threads_from_files_desc_unfiltered);外部调用 3 个(clone, exists, new)。
traverse_directories_for_paths401–434 ↗
async fn traverse_directories_for_paths(
root: PathBuf,
page_size: usize,
anchor: Option<Cursor>,
sort_key: ThreadSortKey,
allowed_sources: &[SessionSource],
provider_matcher:
作用:处理按 年/月/日 分层存放的会话目录。它根据排序字段选择具体扫描方式。
数据流:进去根目录、分页和筛选参数 → 它看 sort_key 是创建时间还是更新时间 → 分别转交给对应的日期目录遍历函数,输出 ThreadsPage。
调用关系:get_threads_in_root 在配置为 NestedByDate 时调用它。它不直接读文件内容,只负责把请求分派到正确算法。
调用图:调用 2 个内部函数(traverse_directories_for_paths_created, traverse_directories_for_paths_updated);被 1 处调用(get_threads_in_root)。
traverse_flat_paths436–469 ↗
async fn traverse_flat_paths(
root: PathBuf,
page_size: usize,
anchor: Option<Cursor>,
sort_key: ThreadSortKey,
allowed_sources: &[SessionSource],
provider_matcher: Option<&Pro
作用:处理所有 rollout 文件直接放在一个目录里的情况。它也是按排序字段分派到不同实现。
数据流:进去根目录、分页和筛选参数 → 根据 sort_key 选择按创建时间或更新时间的扁平扫描 → 返回 ThreadsPage。
调用关系:get_threads_in_root 在配置为 Flat 时调用它。它让同一套列表功能能兼容不同的文件摆放方式。
调用图:调用 2 个内部函数(traverse_flat_paths_created, traverse_flat_paths_updated);被 1 处调用(get_threads_in_root)。
traverse_directories_for_paths_created477–516 ↗
async fn traverse_directories_for_paths_created(
root: PathBuf,
page_size: usize,
anchor: Option<Cursor>,
allowed_sources: &[SessionSource],
provider_matcher: Option<&ProviderMatch
作用:在日期分层目录里,按创建时间从新到旧拿一页线程。创建时间来自文件名,所以可以边扫边收集。
数据流:进去根目录、页大小、游标和筛选条件 → 它建立结果列表和 FilesByCreatedAtVisitor,再让 walk_rollout_files 从新到旧扫描 → 得到 items、下一页游标、扫描数量和是否碰到上限。
调用关系:traverse_directories_for_paths 在 sort_key 是 CreatedAt 时调用它。它依赖 walk_rollout_files 走目录,依赖 build_next_cursor 生成下一页书签。
调用图:调用 3 个内部函数(new, build_next_cursor, walk_rollout_files);被 1 处调用(traverse_directories_for_paths);外部调用 1 个(with_capacity)。
traverse_directories_for_paths_updated526–585 ↗
async fn traverse_directories_for_paths_updated(
root: PathBuf,
page_size: usize,
anchor: Option<Cursor>,
allowed_sources: &[SessionSource],
provider_matcher: Option<&ProviderMatch
作用:在日期分层目录里,按文件最后修改时间排序拿一页线程。因为修改时间不能只靠文件名判断,所以要先收集再排序。
数据流:进去根目录、页大小、游标和筛选条件 → 它收集所有候选文件及修改时间,按更新时间倒序排序,再跳过游标之前的内容并构造 ThreadItem → 输出 ThreadsPage。
调用关系:traverse_directories_for_paths 在 sort_key 是 UpdatedAt 时调用它。它先用 collect_files_by_updated_at 扫描,再用 build_thread_item 读取摘要。
调用图:调用 4 个内部函数(new, build_next_cursor, build_thread_item, collect_files_by_updated_at);被 1 处调用(traverse_directories_for_paths);外部调用 1 个(with_capacity)。
traverse_flat_paths_created587–642 ↗
async fn traverse_flat_paths_created(
root: PathBuf,
page_size: usize,
anchor: Option<Cursor>,
allowed_sources: &[SessionSource],
provider_matcher: Option<&ProviderMatcher<'_>>,
作用:在扁平目录里,按文件名里的创建时间从新到旧拿一页线程。
数据流:进去根目录、页大小、游标和筛选条件 → 它收集并排序所有合法 rollout 文件,逐个按游标跳过、读取修改时间、构造摘要 → 返回一页结果和下一页游标。
调用关系:traverse_flat_paths 在 CreatedAt 排序时调用它。它把 collect_flat_rollout_files 找到的文件交给 build_thread_item 做成用户能看的列表项。
调用图:调用 5 个内部函数(new, build_next_cursor, build_thread_item, collect_flat_rollout_files, file_modified_time);被 1 处调用(traverse_flat_paths);外部调用 1 个(with_capacity)。
traverse_flat_paths_updated644–703 ↗
async fn traverse_flat_paths_updated(
root: PathBuf,
page_size: usize,
anchor: Option<Cursor>,
allowed_sources: &[SessionSource],
provider_matcher: Option<&ProviderMatcher<'_>>,
作用:在扁平目录里,按文件最后修改时间从新到旧拿一页线程。
数据流:进去根目录、页大小、游标和筛选条件 → 它收集每个文件的修改时间,排序,应用分页,再读取文件头部摘要 → 返回 ThreadsPage。
调用关系:traverse_flat_paths 在 UpdatedAt 排序时调用它。它和日期目录版的更新时间排序逻辑类似,只是文件收集方式不同。
调用图:调用 4 个内部函数(new, build_next_cursor, build_thread_item, collect_flat_files_by_updated_at);被 1 处调用(traverse_flat_paths);外部调用 1 个(with_capacity)。
parse_cursor706–720 ↗
fn parse_cursor(token: &str) -> Option<Cursor>
作用:把分页游标字符串解析成 Cursor。它支持标准 RFC3339 时间,也兼容文件名里那种带短横线的时间格式。
数据流:进去一个字符串 → 如果包含旧式分隔符“|”就拒绝;否则尝试按标准时间解析,再尝试按文件名时间解析 → 成功返回 Cursor,失败返回 None。
调用关系:Cursor::deserialize 会调用它。测试里也会用它确认游标格式能被正确识别。
调用图:调用 1 个内部函数(new);被 3 处调用(deserialize, cursor_from_thread_item, cursor_to_anchor_normalizes_timestamp_format);外部调用 1 个(parse)。
build_next_cursor722–734 ↗
fn build_next_cursor(items: &[ThreadItem], sort_key: ThreadSortKey) -> Option<Cursor>
作用:根据当前这一页的最后一个条目,生成下一页用的游标。它相当于在最后一条记录后面夹一张书签。
数据流:进去当前页 items 和排序方式 → 取最后一个 ThreadItem;如果按创建时间,就从文件名解析创建时间;如果按更新时间,就用 item.updated_at → 输出 Cursor 或 None。
调用关系:四种遍历函数在发现还有更多结果时都会调用它。它决定下一次请求从哪里继续。
调用图:调用 2 个内部函数(new, parse_timestamp_uuid_from_filename);被 4 处调用(traverse_directories_for_paths_created, traverse_directories_for_paths_updated, traverse_flat_paths_created, traverse_flat_paths_updated);外部调用 2 个(parse, last)。
build_thread_item736–813 ↗
async fn build_thread_item(
path: PathBuf,
allowed_sources: &[SessionSource],
provider_matcher: Option<&ProviderMatcher<'_>>,
cwd_filters: Option<&[PathBuf]>,
updated_at: Option<St
作用:把一个 rollout 文件变成列表里显示的一条线程摘要。它会同时做来源、模型提供方、工作目录等筛选。
数据流:进去文件路径、筛选条件和可选更新时间 → 它读取文件头部摘要,检查来源、模型、cwd 是否符合要求,再确认有会话元数据和可显示预览 → 成功输出 ThreadItem,不符合就返回 None。
调用关系:各种遍历流程和 read_thread_item_from_rollout 都依赖它。它是“原始文件”变成“列表卡片”的关键步骤。
调用图:调用 1 个内部函数(read_head_summary);被 5 处调用(visit, read_thread_item_from_rollout, traverse_directories_for_paths_updated, traverse_flat_paths_created, traverse_flat_paths_updated);外部调用 1 个(is_empty)。
read_thread_item_from_rollout820–829 ↗
async fn read_thread_item_from_rollout(path: PathBuf) -> Option<ThreadItem>
作用:直接从一个已知 rollout 路径读取线程摘要。适合已经找到文件、不想扫描整个 sessions 目录的场景。
数据流:进去一个文件路径 → 它调用 build_thread_item,但不加来源、模型、目录筛选 → 输出 ThreadItem 或 None。
调用关系:它复用列表页同一套摘要提取规则,保证单文件读取和列表展示看到的信息一致。
调用图:调用 1 个内部函数(build_thread_item)。
collect_dirs_desc833–854 ↗
async fn collect_dirs_desc(parent: &Path, parse: F) -> io::Result<Vec<(T, PathBuf)>>
作用:收集某个目录下符合规则的子目录,并按解析后的值从大到小排序。比如年份、月份、日期都能这样处理。
数据流:进去父目录和一个“怎样把目录名转成数字”的函数 → 它读取子目录,过滤不能解析的名字,排序成倒序 → 返回带排序键和路径的列表。
调用关系:walk_rollout_files 用它依次找年份、月份、日期目录。这样扫描天然从新日期往旧日期走。
调用图:被 1 处调用(walk_rollout_files);外部调用 2 个(new, read_dir)。
collect_files857–876 ↗
async fn collect_files(parent: &Path, parse: F) -> io::Result<Vec<T>>
作用:收集某个目录下符合规则的文件。具体怎样解析文件名,由调用方传进来的函数决定。
数据流:进去父目录和解析函数 → 它读取目录项,只看普通文件,把能解析成功的结果放进列表 → 返回收集到的值。
调用关系:collect_rollout_day_files 用它读取某一天目录里的 rollout 文件。它是一个小型通用文件收集工具。
调用图:被 1 处调用(collect_rollout_day_files);外部调用 2 个(new, read_dir)。
collect_flat_rollout_files878–911 ↗
async fn collect_flat_rollout_files(
root: &Path,
scanned_files: &mut usize,
) -> io::Result<Vec<(OffsetDateTime, Uuid, PathBuf)>>
作用:在扁平目录里找出所有合法 rollout 文件,并按创建时间倒序排列。
数据流:进去根目录和扫描计数器 → 它逐个读取文件,识别是否是 rollout 文件,从文件名解析创建时间和 UUID,增加扫描计数并受上限保护 → 返回排好序的时间、ID、路径列表。
调用关系:traverse_flat_paths_created 调用它。它只负责找到候选文件,不读取文件内容摘要。
调用图:调用 2 个内部函数(from_path, parse_timestamp_uuid_from_filename);被 1 处调用(traverse_flat_paths_created);外部调用 2 个(new, read_dir)。
collect_rollout_day_files913–925 ↗
async fn collect_rollout_day_files(
day_path: &Path,
) -> io::Result<Vec<(OffsetDateTime, Uuid, PathBuf)>>
作用:收集某一天目录里的 rollout 文件,并按创建时间从新到旧排序。
数据流:进去 day_path → 它调用 collect_files 识别 rollout 文件,解析文件名里的时间和 UUID → 返回当天排好序的文件列表。
调用关系:walk_rollout_files 每进入一个日期目录就调用它。它保证同一天内的文件顺序也是稳定的。
调用图:调用 1 个内部函数(collect_files);被 1 处调用(walk_rollout_files)。
parse_timestamp_uuid_from_filename927–943 ↗
fn parse_timestamp_uuid_from_filename(name: &str) -> Option<(OffsetDateTime, Uuid)>
作用:从 rollout 文件名里拆出创建时间和 UUID。文件名就是这个系统给记录文件贴的标签。
数据流:进去文件名字符串 → 它先确认是合法 rollout 文件名,再去掉前后固定部分,从右往左找到能解析成 UUID 的那段,剩下部分按时间格式解析 → 返回时间戳和 UUID。
调用关系:生成下一页游标、收集文件、按 ID 从文件名查找时都会用它。只要需要信任文件名里的时间和身份,就会经过这里。
调用图:被 6 处调用(build_next_cursor, collect_flat_files_by_updated_at, collect_flat_rollout_files, find_rollout_path_by_id_from_filenames, builder_from_items, thread_item_sort_key);外部调用 3 个(parse, parse_rollout_file_name, format_description!)。
collect_files_by_updated_at951–962 ↗
async fn collect_files_by_updated_at(
root: &Path,
scanned_files: &mut usize,
) -> io::Result<Vec<ThreadCandidate>>
作用:在日期分层目录里收集所有文件候选项,并带上文件修改时间。
数据流:进去根目录和扫描计数器 → 它创建 FilesByUpdatedAtVisitor,交给 walk_rollout_files 遍历 → 返回 ThreadCandidate 列表。
调用关系:traverse_directories_for_paths_updated 调用它。它把复杂的目录遍历复用起来,只改变每个文件被访问时做的事。
调用图:调用 1 个内部函数(walk_rollout_files);被 1 处调用(traverse_directories_for_paths_updated);外部调用 1 个(new)。
collect_flat_files_by_updated_at964–1004 ↗
async fn collect_flat_files_by_updated_at(
root: &Path,
scanned_files: &mut usize,
) -> io::Result<Vec<ThreadCandidate>>
作用:在扁平目录里收集所有 rollout 文件,并记录它们的最后修改时间。
数据流:进去根目录和扫描计数器 → 它读取目录文件,识别 rollout 文件,解析 UUID,读取文件修改时间 → 返回候选项列表。
调用关系:traverse_flat_paths_updated 调用它。后续会按这些更新时间排序,再生成真正的 ThreadItem。
调用图:调用 3 个内部函数(from_path, file_modified_time, parse_timestamp_uuid_from_filename);被 1 处调用(traverse_flat_paths_updated);外部调用 2 个(new, read_dir)。
walk_rollout_files1006–1044 ↗
async fn walk_rollout_files(
root: &Path,
scanned_files: &mut usize,
visitor: &mut impl RolloutFileVisitor,
) -> io::Result<()>
作用:按 年/月/日 的目录结构,从新到旧遍历 rollout 文件。它自己不决定怎么处理文件,而是把文件交给 visitor。
数据流:进去根目录、扫描计数器和访问器 → 它先倒序找年份,再找月份、日期,最后取每天文件;每遇到一个文件就增加扫描计数并调用 visitor.visit → 扫完、达到上限或 visitor 要求停止后结束。
调用关系:创建时间列表和更新时间候选收集都会调用它。它像传送带,具体加工由不同 visitor 完成。
调用图:调用 2 个内部函数(collect_dirs_desc, collect_rollout_day_files);被 2 处调用(collect_files_by_updated_at, traverse_directories_for_paths_created);外部调用 1 个(visit)。
ProviderMatcher::new1052–1062 ↗
fn new(filters: &'a [String], default_provider: &'a str) -> Option<Self>
作用:创建一个模型提供方筛选器。模型提供方可以理解成“这次对话用的是哪家模型服务”。
数据流:进去用户指定的 provider 列表和默认 provider → 如果列表为空就表示不需要筛选,返回 None;否则记录筛选列表,并记住默认 provider 是否也被允许 → 返回 ProviderMatcher。
调用关系:get_threads_in_root 在准备筛选条件时调用它。build_thread_item 后面会用 ProviderMatcher::matches 判断单个会话是否保留。
ProviderMatcher::matches1064–1069 ↗
fn matches(&self, session_provider: Option<&str>) -> bool
作用:判断某个会话的模型提供方是否符合筛选条件。没有写明 provider 的旧会话,会按默认 provider 处理。
数据流:进去会话里可有可无的 provider 名称 → 有名称就和筛选列表逐个比较;没有名称就看默认 provider 是否在筛选列表中 → 返回是否匹配。
调用关系:build_thread_item 在生成列表项前调用它。这样不符合模型筛选的文件不会出现在结果里。
read_head_summary1072–1154 ↗
async fn read_head_summary(path: &Path, head_limit: usize) -> io::Result<HeadTailSummary>
作用:读取 rollout 文件开头的一小段,提取列表展示需要的摘要。它不会读完整个大文件,避免浪费。
数据流:进去文件路径和最多先读多少条记录 → 它打开 JSONL 行读取器,逐行解析 SessionMeta、事件消息等,拿到线程 ID、cwd、git 信息、模型、创建时间、预览和第一条用户消息 → 返回 HeadTailSummary。
调用关系:build_thread_item 调用它。它还会调用 event_msg_preview 从用户消息或目标更新事件里抽出可显示的文字。
调用图:调用 2 个内部函数(open_rollout_line_reader, event_msg_preview);被 1 处调用(build_thread_item);外部调用 2 个(default, from_str)。
read_head_for_summary1158–1195 ↗
async fn read_head_for_summary(path: &Path) -> io::Result<Vec<serde_json::Value>>
作用:读取 rollout 文件开头若干条可用于摘要的原始 JSON 值。它主要给需要会话元数据的调用方复用。
数据流:进去文件路径 → 它打开行读取器,最多读取 HEAD_RECORD_LIMIT 条有效记录,只保留 SessionMeta、ResponseItem 和代理通信等可放入摘要的内容 → 返回 JSON 值列表。
调用关系:read_session_meta_line 调用它来拿第一条会话元数据。相关测试也用它检查文件头部保存的信息是否正确。
调用图:调用 1 个内部函数(open_rollout_line_reader);被 3 处调用(read_session_meta_line, test_base_instructions_missing_in_meta_defaults_to_null, test_base_instructions_present_in_meta_is_preserved);外部调用 2 个(new, to_value)。
strip_user_message_prefix1197–1202 ↗
fn strip_user_message_prefix(text: &str) -> &str
作用:去掉用户消息里系统加的固定前缀,只留下真正给人看的内容。
数据流:进去一段文本 → 如果找到 USER_MESSAGE_BEGIN 标记,就截掉标记前后的包装并修剪空白;找不到就只修剪空白 → 返回清理后的字符串切片。
调用关系:event_msg_preview 在处理普通用户消息时调用它。它让列表预览不会显示内部标记。
调用图:被 1 处调用(event_msg_preview)。
event_msg_preview1204–1227 ↗
fn event_msg_preview(event: &EventMsg) -> Option<String>
作用:从事件消息里挑出适合列表展示的一小段预览文字。比如用户第一句话,或者线程目标。
数据流:进去一个 EventMsg → 如果是用户消息,就清理文本;文本为空但有图片时返回“[Image]”;如果是目标更新,就返回目标文字;其他事件返回 None → 输出可选预览。
调用关系:read_head_summary 调用它。它决定历史列表卡片上那句“这次对话大概是什么”的文字来源。
调用图:调用 1 个内部函数(strip_user_message_prefix);被 1 处调用(read_head_summary)。
read_session_meta_line1231–1245 ↗
async fn read_session_meta_line(path: &Path) -> io::Result<SessionMetaLine>
作用:从 rollout 文件头部读取 SessionMetaLine,也就是这次会话的基础身份证信息。
数据流:进去文件路径 → 它先调用 read_head_for_summary 读取头部;如果没有内容或第一条不是会话元数据,就返回错误;否则反序列化成 SessionMetaLine → 输出元数据。
调用关系:按 ID 找文件时会用它验证数据库给出的路径是否真的属于目标线程。其他需要根据线程拿 cwd 等信息的逻辑也会调用它。
调用图:调用 1 个内部函数(read_head_for_summary);被 3 处调用(find_thread_path_by_id_str_in_subdir, find_thread_meta_by_name_str, read_repair_rollout_path);外部调用 2 个(other, format!)。
file_modified_time1247–1251 ↗
async fn file_modified_time(path: &Path) -> io::Result<Option<OffsetDateTime>>
作用:读取 rollout 文件的最后修改时间,并把精度截到毫秒。这样时间格式更稳定,适合分页和展示。
数据流:进去文件路径 → 它调用 compression 模块的文件修改时间读取函数,再用 truncate_to_millis 处理纳秒部分 → 返回可选的 OffsetDateTime。
调用关系:按更新时间排序和构建列表项时都会调用它。它把底层文件系统时间整理成列表模块可用的时间。
调用图:调用 1 个内部函数(file_modified_time);被 4 处调用(visit, visit, collect_flat_files_by_updated_at, traverse_flat_paths_created)。
format_rfc33391253–1255 ↗
fn format_rfc3339(dt: OffsetDateTime) -> Option<String>
作用:把时间戳格式化成 RFC3339 字符串。RFC3339 是一种常见的、机器和人都容易识别的时间格式。
数据流:进去 OffsetDateTime → 它尝试按 RFC3339 格式转换 → 成功返回字符串,失败返回 None。
调用关系:各个遍历流程在把文件修改时间放进 ThreadItem 时会用它。Cursor 序列化也使用同类格式。
调用图:外部调用 1 个(format)。
truncate_to_millis1257–1260 ↗
fn truncate_to_millis(dt: OffsetDateTime) -> Option<OffsetDateTime>
作用:把时间戳的纳秒精度截到毫秒。这样可以避免不同系统纳秒精度不一致导致排序或比较不稳定。
数据流:进去一个高精度时间 → 它计算毫秒对应的纳秒值,并替换原时间的纳秒部分 → 返回新时间,失败则返回 None。
调用关系:file_modified_time 调用它。所有基于文件修改时间的列表展示都会间接受到它的规范化处理。
调用图:外部调用 2 个(nanosecond, replace_nanosecond)。
find_thread_path_by_id_str_in_subdir1262–1424 ↗
async fn find_thread_path_by_id_str_in_subdir(
codex_home: &Path,
subdir: &str,
id_str: &str,
state_db_ctx: Option<&codex_state::StateRuntime>,
) -> io::Result<Option<PathBuf>>
作用:在指定子目录里按线程 UUID 找到 rollout 文件路径。它优先相信数据库,但会验证,不可靠时再扫文件。
数据流:进去 codex_home、子目录名、ID 字符串和可选状态数据库 → 它先校验 UUID;再尝试从数据库查路径并读取元数据核对;如果数据库没有、过期或不可信,就先按文件名扫描,再必要时全文搜索 ID;找到后还会尝试修复数据库记录 → 返回路径、None 或错误。
调用关系:find_thread_path_by_id_str 和 find_archived_thread_path_by_id_str 都调用它。它把数据库、文件系统扫描、搜索和读修复串成一个稳妥的查找流程。
调用图:调用 4 个内部函数(from_string, find_rollout_path_by_id_from_filenames, read_session_meta_line, read_repair_rollout_path);被 2 处调用(find_archived_thread_path_by_id_str, find_thread_path_by_id_str);外部调用 11 个(default, new, to_path_buf, parse_str, record_fallback, existing_rollout_path, run, debug!, error!, warn! (+1 more))。
find_rollout_path_by_id_from_filenames1426–1464 ↗
async fn find_rollout_path_by_id_from_filenames(
root: &Path,
id_str: &str,
) -> io::Result<Option<PathBuf>>
作用:只靠文件名里的 UUID,在目录树里查找对应 rollout 文件。它比全文搜索更直接、更便宜。
数据流:进去根目录和目标 ID 字符串 → 它把目标解析成 UUID,用栈遍历目录树,遇到 rollout 文件就从文件名解析 UUID → 找到匹配就返回路径,全部扫完没有则返回 None。
调用关系:find_thread_path_by_id_str_in_subdir 在数据库查不到或不可信时先调用它。只有它也找不到,才会走更重的全文搜索。
调用图:调用 2 个内部函数(from_path, parse_timestamp_uuid_from_filename);被 1 处调用(find_thread_path_by_id_str_in_subdir);外部调用 3 个(parse_str, read_dir, vec!)。
find_thread_path_by_id_str1469–1475 ↗
async fn find_thread_path_by_id_str(
codex_home: &Path,
id_str: &str,
state_db_ctx: Option<&codex_state::StateRuntime>,
) -> io::Result<Option<PathBuf>>
作用:在普通 sessions 目录里按线程 ID 查找 rollout 文件。调用者不用关心子目录名。
数据流:进去 codex_home、ID 字符串和可选状态数据库 → 它把查找范围固定为 SESSIONS_SUBDIR → 返回找到的路径或 None。
调用关系:清理旧快照、按名称查线程元数据等流程会调用它。它是查找未归档线程文件的公开入口。
调用图:调用 1 个内部函数(find_thread_path_by_id_str_in_subdir);被 2 处调用(cleanup_stale_snapshots, find_thread_meta_by_name_str)。
find_archived_thread_path_by_id_str1478–1485 ↗
async fn find_archived_thread_path_by_id_str(
codex_home: &Path,
id_str: &str,
state_db_ctx: Option<&codex_state::StateRuntime>,
) -> io::Result<Option<PathBuf>>
作用:在 archived sessions 目录里按线程 ID 查找已归档的 rollout 文件。
数据流:进去 codex_home、ID 字符串和可选状态数据库 → 它把查找范围固定为 ARCHIVED_SESSIONS_SUBDIR → 返回已归档线程的路径或 None。
调用关系:需要访问归档会话时会调用它。它复用 find_thread_path_by_id_str_in_subdir,只是换了搜索目录。
调用图:调用 1 个内部函数(find_thread_path_by_id_str_in_subdir)。
rollout_date_parts1488–1495 ↗
fn rollout_date_parts(file_name: &OsStr) -> Option<(String, String, String)>
作用:从 rollout 文件名里取出年月日三段目录名。它用于把文件按日期放回或识别到对应目录。
数据流:进去一个文件名 → 它转成字符串,确认以 rollout- 开头,再截取前 10 位日期,拆成年、月、日 → 成功返回三个字符串,失败返回 None。
调用关系:它是一个小工具函数,服务于需要根据文件名推导日期路径的代码。它不读文件内容,只看名字。
调用图:外部调用 1 个(to_string_lossy)。
rollout/src/recorder.rs源码 ↗
可以把它理解成“会话黑匣子”。用户和模型对话时,系统会不断产生记录项,这个文件把它们按顺序存到 rollout 文件里;以后要继续旧会话,就再把这些记录读回来。为了不让写磁盘拖慢对话,它用后台写入任务:前台只把要写的东西放进队列,后台慢慢写。新会话一开始不会马上建文件,等真正需要保存时才创建;恢复旧会话则会直接打开原文件继续追加。它还处理压缩过的 rollout、旧格式里的无效 ghost_snapshot 记录、Git 信息、分页列表、搜索过滤,以及 SQLite 状态库和文件系统之间不一致时的修补。重点是:即使数据库坏了或缺记录,用户仍然能靠文件找回历史。
RolloutWriterTask::new120–125 ↗
RolloutWriterTask::set_handle128–134 ↗
fn set_handle(&self, handle: JoinHandle<()>)
作用:把已经启动的后台写入任务保存起来,避免任务句柄被丢掉。它像把工人的联系方式登记到值班表里。
数据流:进去一个 JoinHandle(后台任务句柄)→ 它加锁打开内部存放位置,把句柄写进去 → 不返回业务结果,只改变 RolloutWriterTask 内部状态。
调用关系:RolloutRecorder::new 启动 rollout_writer 后调用它,把后台任务和 recorder 的生命周期绑在一起。
RolloutWriterTask::mark_failed137–143 ↗
fn mark_failed(&self, err: &IoError)
作用:记录后台写入任务已经彻底失败,以及失败原因。之后前台 API 再发送命令失败时,就能返回真正的磁盘错误,而不是含糊地说队列断了。
数据流:进去一个输入输出错误 → 它复制出一份可保存的错误对象,放到 terminal_failure 里 → 以后 terminal_failure 可以读到这个失败。
调用关系:RolloutRecorder::new 里启动的后台任务如果以错误退出,会调用它;它依赖 clone_io_error 来复制错误。
调用图:调用 1 个内部函数(clone_io_error);外部调用 1 个(new)。
RolloutWriterTask::terminal_failure146–152 ↗
fn terminal_failure(&self) -> Option<IoError>
作用:查询后台写入任务有没有已经彻底失败。调用者用它来给用户返回更准确的错误信息。
数据流:进去没有参数 → 它加锁读取保存的失败原因,如果有就复制一份 → 出来是一个可选错误,没有失败就返回空。
调用关系:RolloutRecorder::record_canonical_items、persist、flush、shutdown 在队列发送失败或等待失败时会用它兜底说明原因。
clone_io_error155–157 ↗
fn clone_io_error(err: &IoError) -> IoError
作用:复制一个输入输出错误。Rust 的 IoError 不能直接克隆,所以这里按错误种类和文字重新造一个。
数据流:进去一个 IoError → 读取它的错误类别和文字说明 → 出来一个新的 IoError,内容尽量和原错误一致。
调用关系:RolloutWriterTask::mark_failed 和 terminal_failure 需要保存或返回错误副本时会间接用到它。
调用图:被 1 处调用(mark_failed);外部调用 3 个(kind, new, to_string)。
RolloutRecorderParams::new160–179 ↗
fn new(
conversation_id: ThreadId,
forked_from_id: Option<ThreadId>,
parent_thread_id: Option<ThreadId>,
source: SessionSource,
thread_source: Option<ThreadSour
作用:创建“新会话录制器”的参数包。调用者不用手动拼一堆字段,只要把会话 ID、来源、基础指令和工具列表交进来。
数据流:进去新会话的 ID、父子关系、来源、基础说明、动态工具 → 它把这些装进 Create 变体,并默认没有多智能体版本 → 出来一个 RolloutRecorderParams。
调用关系:创建线程的流程和相关测试会调用它,然后把参数交给 RolloutRecorder::new 真正创建录制器。
调用图:被 4 处调用(find_locates_rollout_file_written_by_recorder, persist_reports_filesystem_error_and_retries_buffered_items, recorder_materializes_on_flush_with_pending_items, create_thread)。
RolloutRecorderParams::with_multi_agent_version181–193 ↗
fn with_multi_agent_version(
mut self,
multi_agent_version: Option<MultiAgentVersion>,
) -> Self
作用:给新会话参数补上“多智能体版本”信息。它只对新建会话生效,对恢复旧会话不会乱改。
数据流:进去已有参数和一个可选版本 → 如果参数是 Create,就把内部 multi_agent_version 改成传入值 → 出来仍是同一个参数对象,方便链式调用。
调用关系:通常在 RolloutRecorderParams::new 之后使用,再交给 RolloutRecorder::new。
RolloutRecorderParams::resume195–197 ↗
fn resume(path: PathBuf) -> Self
作用:创建“恢复旧会话”的参数包。它告诉录制器:不要新建文件,而是打开指定 rollout 文件继续追加。
数据流:进去一个文件路径 → 它把路径装进 Resume 变体 → 出来一个 RolloutRecorderParams。
调用关系:恢复线程的流程会调用它,然后传给 RolloutRecorder::new。
调用图:被 2 处调用(resume_materializes_compressed_rollout_path, resume_thread)。
RolloutRecorder::list_threads215–244 ↗
async fn list_threads(
state_db_ctx: Option<StateDbHandle>,
config: &impl RolloutConfigView,
page_size: usize,
cursor: Option<&Cursor>,
sort_key: ThreadSortKey,
作用:列出普通的、未归档的历史会话线程。它支持分页、排序、来源过滤、模型过滤、目录过滤和搜索。
数据流:进去配置、分页游标、排序方式、过滤条件和可选状态库 → 它把请求标记为“活跃线程”和“允许扫描修复” → 出来一个 ThreadsPage,也就是一页线程列表。
调用关系:外部列表接口和多项测试会调用它;真正复杂的数据库/文件兜底逻辑交给 RolloutRecorder::list_threads_with_db_fallback。
调用图:被 9 处调用(thread_list_respects_search_term_filter, list_threads_db_disabled_does_not_skip_paginated_items, list_threads_db_enabled_drops_missing_rollout_paths, list_threads_db_enabled_repairs_stale_rollout_paths, list_threads_default_filter_returns_filesystem_scan_results, list_threads_metadata_filter_overlays_state_db_list_metadata, list_threads_search_repairs_stale_state_db_hits_before_returning, list_threads_state_db_only_skips_jsonl_repair_scan, list_rollout_threads);外部调用 1 个(list_threads_with_db_fallback)。
RolloutRecorder::list_threads_from_state_db247–276 ↗
async fn list_threads_from_state_db(
state_db_ctx: Option<StateDbHandle>,
config: &impl RolloutConfigView,
page_size: usize,
cursor: Option<&Cursor>,
sort_key:
作用:只从状态数据库列出普通历史线程,不主动扫描 JSONL 文件修复。适合调用者明确想要数据库视图时使用。
数据流:进去和 list_threads 类似的分页与过滤条件 → 它选择“活跃线程”和“只查状态库”模式 → 出来数据库转换后的线程页,数据库不可用时返回默认空页。
调用关系:列表接口和测试会调用它;它同样把实际查询交给 RolloutRecorder::list_threads_with_db_fallback。
调用图:被 4 处调用(list_threads_default_filter_returns_filesystem_scan_results, list_threads_search_repairs_stale_state_db_hits_before_returning, list_threads_state_db_only_skips_jsonl_repair_scan, list_rollout_threads);外部调用 1 个(list_threads_with_db_fallback)。
RolloutRecorder::list_archived_threads280–309 ↗
async fn list_archived_threads(
state_db_ctx: Option<StateDbHandle>,
config: &impl RolloutConfigView,
page_size: usize,
cursor: Option<&Cursor>,
sort_key: Threa
作用:列出已经归档的历史会话。它和普通列表类似,只是目标目录换成归档区。
数据流:进去配置、分页、排序和过滤条件 → 它把 archive_filter 设为 Archived,并允许扫描文件修复数据库 → 出来一页归档线程。
调用关系:归档列表接口会调用它;实际工作由 RolloutRecorder::list_threads_with_db_fallback 完成。
调用图:被 1 处调用(list_rollout_threads);外部调用 1 个(list_threads_with_db_fallback)。
RolloutRecorder::list_archived_threads_from_state_db312–341 ↗
async fn list_archived_threads_from_state_db(
state_db_ctx: Option<StateDbHandle>,
config: &impl RolloutConfigView,
page_size: usize,
cursor: Option<&Cursor>,
s
作用:只从状态数据库列出归档会话,不扫归档文件夹做修复。用于需要快速或严格数据库视图的地方。
数据流:进去归档列表所需的分页和过滤条件 → 它选择“归档线程”和“只查状态库” → 出来一页归档线程,数据库无结果时为空。
调用关系:归档列表入口会调用它;底层仍走 RolloutRecorder::list_threads_with_db_fallback。
调用图:被 1 处调用(list_rollout_threads);外部调用 1 个(list_threads_with_db_fallback)。
RolloutRecorder::list_threads_with_db_fallback344–583 ↗
async fn list_threads_with_db_fallback(
state_db_ctx: Option<StateDbHandle>,
config: &impl RolloutConfigView,
page_size: usize,
cursor: Option<&Cursor>,
sort_ke
作用:这是列线程的总调度:优先利用 SQLite 状态库,但状态库缺失、过期或出错时,会扫 rollout 文件兜底。它保证“能列出来”比“只信数据库”更重要。
数据流:进去 Codex 主目录、分页、排序、过滤、归档标记、修复模式和状态库句柄 → 它先根据模式决定查库还是扫文件,必要时用文件结果修补数据库,再决定返回数据库页还是文件页 → 出来 ThreadsPage,并可能顺手修复状态库记录。
调用关系:四个公开列表函数都调用它;它会把文件扫描交给 list_threads_from_files_asc 或 list_threads_from_files_desc,把缺失元数据补齐交给 fill_missing_thread_item_metadata_from_state_db。
调用图:调用 7 个内部函数(fill_missing_thread_item_metadata_from_state_db, list_threads_from_files_asc, list_threads_from_files_desc, page_from_filesystem_scan, list_threads_db, read_repair_rollout_path, reconcile_rollout);外部调用 7 个(is_empty, record_fallback, matches!, codex_home, default, error!, warn!)。
RolloutRecorder::find_latest_thread_path587–664 ↗
async fn find_latest_thread_path(
state_db_ctx: Option<StateDbHandle>,
config: &impl RolloutConfigView,
page_size: usize,
cursor: Option<&Cursor>,
sort_key: Thr
作用:找出最新一个可以恢复的会话文件路径,可以按工作目录过滤。用户点“继续上次会话”时,这类逻辑就很关键。
数据流:进去状态库、配置、分页大小、排序依据、来源/模型过滤和可选 cwd → 它先尝试从数据库从新到旧找,找不到或数据库出错再扫文件 → 出来一个可选路径,找不到就是空。
调用关系:恢复会话前会用它定位文件;它调用 select_resume_path_from_db_page 或 select_resume_path 来判断候选文件是否匹配目录。
调用图:调用 4 个内部函数(get_threads, select_resume_path, select_resume_path_from_db_page, list_threads_db);外部调用 2 个(record_fallback, codex_home)。
RolloutRecorder::new672–784 ↗
async fn new(
config: &impl RolloutConfigView,
params: RolloutRecorderParams,
) -> std::io::Result<Self>
作用:创建真正的会话录制器。新会话会先准备文件名和元数据,旧会话会打开原文件,然后都启动一个后台写入任务。
数据流:进去配置和 Create/Resume 参数 → 新会话预计算路径和 SessionMeta,恢复会话则把可能压缩的文件转成可追加文件并打开;之后建立消息队列并启动 rollout_writer → 出来一个 RolloutRecorder,前台可继续发送记录。
调用关系:创建线程和恢复线程都会调用它;它调用 precompute_log_file_info、RolloutWriterTask::new,并把写入工作交给 rollout_writer。
调用图:调用 5 个内部函数(originator, materialize_rollout_for_append, new, precompute_log_file_info, rollout_writer);被 6 处调用(find_locates_rollout_file_written_by_recorder, resume_materializes_compressed_rollout_path, persist_reports_filesystem_error_and_retries_buffered_items, recorder_materializes_on_flush_with_pending_items, create_thread, resume_thread);外部调用 10 个(clone, new, env!, error!, format_description!, cwd, generate_memories, model_provider_id, new, spawn)。
RolloutRecorder::rollout_path786–788 ↗
fn rollout_path(&self) -> &Path
作用:返回当前录制器对应的 rollout 文件路径。调用者可以用它展示、索引或传给其他模块。
数据流:进去没有额外数据 → 它把内部 PathBuf 以只读 Path 形式借出去 → 不改变任何状态。
调用关系:创建好 RolloutRecorder 后,外部需要知道文件位置时会调用它。
调用图:外部调用 1 个(as_path)。
RolloutRecorder::record_canonical_items790–802 ↗
async fn record_canonical_items(&self, items: &[RolloutItem]) -> std::io::Result<()>
作用:把一批已经规范化的会话记录放进后台写入队列。它不直接写磁盘,避免卡住正在进行的对话。
数据流:进去一组 RolloutItem → 如果为空就什么都不做;否则复制成 Vec,发送 AddItems 命令给后台任务 → 成功返回空结果,失败时返回队列或后台写入错误。
调用关系:实时会话产生新记录时会调用它;后台 rollout_writer 收到 AddItems 后再交给 RolloutWriterState 保存和可能刷新。
RolloutRecorder::persist808–823 ↗
RolloutRecorder::flush829–844 ↗
RolloutRecorder::load_rollout_items846–903 ↗
async fn load_rollout_items(
path: &Path,
) -> std::io::Result<(Vec<RolloutItem>, Option<ThreadId>, usize)>
作用:从 rollout 文件里读回所有会话记录,用于恢复会话或提取元数据。它能跳过空行、坏 JSON 行和旧版本遗留的 ghost_snapshot 垃圾记录。
数据流:进去一个文件路径 → 它按行读取,逐行解析 JSON,再转成 RolloutItem;遇到解析错误会计数但尽量继续 → 出来记录列表、首次读到的线程 ID、解析错误数量;空文件会报错。
调用关系:恢复历史、元数据提取、压缩文件测试和 cwd 匹配都会调用它;它内部用 strip_legacy_ghost_snapshot_rollout_line 清理旧格式。
调用图:调用 2 个内部函数(open_rollout_line_reader, strip_legacy_ghost_snapshot_rollout_line);被 13 处调用(thread_id_from_rollout, sample, append_rollout_item_materializes_compressed_rollout, load_rollout_items_reads_compressed_rollout, resume_materializes_compressed_rollout_path, worker_skips_existing_compressed_archived_rollouts, extract_metadata_from_rollout, resume_candidate_matches_cwd, load_rollout_items_filters_legacy_ghost_snapshots_from_compaction_history, load_rollout_items_preserves_legacy_guardian_assessment_lines (+3 more));外部调用 6 个(new, other, from_str, trace!, debug!, warn!)。
RolloutRecorder::get_rollout_history905–920 ↗
async fn get_rollout_history(path: &Path) -> std::io::Result<InitialHistory>
作用:把 rollout 文件转换成启动会话需要的历史对象。它是“从文件恢复对话”的高层入口之一。
数据流:进去一个 rollout 路径 → 它调用 load_rollout_items 读出记录和线程 ID;没有线程 ID 就报错,有记录就包装成 ResumedHistory → 出来 InitialHistory::Resumed 或空历史标记。
调用关系:恢复线程、回滚和历史注入相关流程会调用它;底层读取交给 RolloutRecorder::load_rollout_items。
调用图:被 10 处调用(thread_inject_items_adds_raw_response_items_to_thread_history, record_context_updates_and_set_reference_context_item_persists_baseline_without_emitting_diffs, record_context_updates_and_set_reference_context_item_persists_full_reinjection_to_rollout, record_context_updates_and_set_reference_context_item_persists_split_file_system_policy_to_rollout, thread_rollback_drops_last_turn_from_history, thread_rollback_persists_marker_and_replays_cumulatively, interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_history, interrupted_fork_snapshot_preserves_explicit_turn_id, interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_source, resume_materializes_compressed_rollout_path);外部调用 4 个(load_rollout_items, plain_rollout_path, info!, Resumed)。
RolloutRecorder::shutdown925–947 ↗
async fn shutdown(&self) -> std::io::Result<()>
作用:关闭录制器前,先让后台把剩余记录写完。写不完时不会强行杀掉后台,方便之后继续重试。
数据流:进去没有业务参数 → 它发送 Shutdown 命令并等待回执;后台写完后才退出循环 → 出来成功或错误,发送失败时会尽量返回真实后台失败原因。
调用关系:会话结束或资源清理时调用;rollout_writer 收到 Shutdown 后会调用 RolloutWriterState::shutdown。
strip_legacy_ghost_snapshot_rollout_line950–967 ↗
fn strip_legacy_ghost_snapshot_rollout_line(value: &mut Value) -> bool
作用:清理旧版本 rollout 里的 ghost_snapshot 记录。这个记录现在不该参与恢复,否则可能污染历史。
数据流:进去一条 JSON 值 → 如果整条是 ghost_snapshot 响应项,就告诉调用者跳过;如果它藏在 compacted 的 replacement_history 里,就把它从数组里删掉 → 出来一个布尔值,表示整条是否应跳过。
调用关系:RolloutRecorder::load_rollout_items 解析每行前会调用它;它用 is_legacy_ghost_snapshot_response_item 判断具体项目。
调用图:被 1 处调用(load_rollout_items);外部调用 2 个(get, get_mut)。
is_legacy_ghost_snapshot_response_item969–971 ↗
fn is_legacy_ghost_snapshot_response_item(value: &Value) -> bool
作用:判断一个 JSON 项是不是旧版 ghost_snapshot 响应项。它是清理旧历史的小判断器。
数据流:进去一个 JSON 值 → 它查看 type 字段是否等于 ghost_snapshot → 出来 true 或 false。
调用关系:strip_legacy_ghost_snapshot_rollout_line 用它决定跳过整行或从压缩历史数组里移除某项。
调用图:外部调用 1 个(get)。
truncate_fs_page973–992 ↗
fn truncate_fs_page(
mut page: ThreadsPage,
page_size: usize,
sort_key: ThreadSortKey,
) -> ThreadsPage
作用:把文件系统扫描出来的一页结果裁到指定大小,并生成下一页游标。这样分页不会一次返回太多。
数据流:进去一页 ThreadsPage、目标页大小和排序键 → 如果项目太多就截断,并根据最后一项的时间生成 next_cursor → 出来裁剪后的页面。
调用关系:page_from_filesystem_scan 在倒序文件扫描结果需要收缩时调用它。
调用图:被 1 处调用(page_from_filesystem_scan)。
page_from_filesystem_scan994–1004 ↗
fn page_from_filesystem_scan(
page: ThreadsPage,
sort_direction: SortDirection,
page_size: usize,
sort_key: ThreadSortKey,
) -> ThreadsPage
作用:把文件扫描结果调整成最终可返回的分页结果。倒序列表可能被预取过多,所以需要裁剪。
数据流:进去文件扫描页、排序方向、页大小和排序键 → 升序直接返回,倒序调用 truncate_fs_page 裁剪 → 出来最终页面。
调用关系:RolloutRecorder::list_threads_with_db_fallback 在数据库不可用、过滤兜底或数据库失败时调用它。
调用图:调用 1 个内部函数(truncate_fs_page);被 1 处调用(list_threads_with_db_fallback)。
fill_missing_thread_item_metadata_from_state_db1006–1032 ↗
async fn fill_missing_thread_item_metadata_from_state_db(
state_db_ctx: Option<&StateRuntime>,
mut page: ThreadsPage,
) -> ThreadsPage
作用:用状态数据库补齐文件扫描结果里缺失的标题、目录、模型等信息。文件扫描快但信息可能少,数据库能补细节。
数据流:进去可选状态库和一页 ThreadItem → 没有状态库就原样返回;有状态库就逐个按 thread_id 查询,并把缺的字段补上 → 出来信息更完整的页面。
调用关系:list_threads_with_db_fallback 在返回文件扫描结果前会调用它;字段合并交给 fill_missing_thread_item_metadata。
调用图:调用 2 个内部函数(fill_missing_thread_item_metadata, thread_item_from_state_metadata);被 1 处调用(list_threads_with_db_fallback);外部调用 1 个(warn!)。
fill_missing_thread_item_metadata1034–1096 ↗
fn fill_missing_thread_item_metadata(item: &mut ThreadItem, state_item: ThreadItem)
作用:把一个线程条目缺失的字段,从状态库版本里补进去。已有的文件扫描信息不会被随便覆盖,除非某些 Git 字段有更明确的新值。
数据流:进去一个可修改的 ThreadItem 和一个来自状态库的 ThreadItem → 它逐项检查 first_user_message、preview、cwd、来源、模型等字段,缺什么补什么 → 出来原 ThreadItem 被更新。
调用关系:fill_missing_thread_item_metadata_from_state_db 查询到状态库元数据后调用它。
调用图:被 1 处调用(fill_missing_thread_item_metadata_from_state_db)。
list_threads_from_files_desc1099–1173 ↗
async fn list_threads_from_files_desc(
codex_home: &Path,
page_size: usize,
cursor: Option<&Cursor>,
sort_key: ThreadSortKey,
allowed_sources: &[SessionSource],
model_providers
作用:按从新到旧的顺序从 rollout 文件列线程,并可按搜索词过滤。搜索时会多扫一些文件,避免第一页因为过滤后不够满。
数据流:进去目录、分页、排序键、过滤条件、是否归档和搜索词 → 没有搜索词就直接扫;有搜索词就循环扫较大的批次,再按标题过滤并生成游标 → 出来一页倒序线程。
调用关系:list_threads_with_db_fallback 倒序文件兜底时调用它;升序扫描 list_threads_from_files_asc 也借它先拿全量倒序数据。
调用图:调用 2 个内部函数(filter_thread_items_by_search_term, list_threads_from_files_desc_unfiltered);被 2 处调用(list_threads_with_db_fallback, list_threads_from_files_asc);外部调用 1 个(new)。
list_threads_from_files_desc_unfiltered1176–1216 ↗
async fn list_threads_from_files_desc_unfiltered(
codex_home: &Path,
page_size: usize,
cursor: Option<&Cursor>,
sort_key: ThreadSortKey,
allowed_sources: &[SessionSource],
mode
作用:从文件系统列出倒序线程,但不做搜索词过滤。它只决定扫普通会话目录还是归档目录。
数据流:进去 Codex 主目录、分页和过滤条件 → 如果是归档就扫 archived sessions 根目录,否则扫正常 sessions 目录 → 出来未按搜索词过滤的 ThreadsPage。
调用关系:list_threads_from_files_desc 调用它获取基础文件列表。
调用图:调用 2 个内部函数(get_threads, get_threads_in_root);被 1 处调用(list_threads_from_files_desc);外部调用 1 个(join)。
list_threads_from_files_asc1219–1293 ↗
async fn list_threads_from_files_asc(
codex_home: &Path,
page_size: usize,
cursor: Option<&Cursor>,
sort_key: ThreadSortKey,
allowed_sources: &[SessionSource],
model_providers:
作用:按从旧到新的顺序从文件系统列线程。因为底层更擅长倒序,它会先收集倒序结果,再重新排序成升序。
数据流:进去分页、过滤、归档和搜索条件 → 它分批扫出所有匹配文件,按搜索词过滤,再按创建或更新时间升序排序,最后按游标和页大小截取 → 出来一页升序线程。
调用关系:list_threads_with_db_fallback 在用户要求升序列表时调用它;它内部复用 list_threads_from_files_desc。
调用图:调用 2 个内部函数(filter_thread_items_by_search_term, list_threads_from_files_desc);被 1 处调用(list_threads_with_db_fallback);外部调用 1 个(new)。
filter_thread_items_by_search_term1295–1318 ↗
async fn filter_thread_items_by_search_term(
codex_home: &Path,
items: &mut Vec<ThreadItem>,
search_term: Option<&str>,
) -> std::io::Result<()>
作用:按线程标题过滤文件扫描结果。因为文件条目本身可能没有标题,它会去旁边的 session index 查标题。
数据流:进去 Codex 主目录、线程条目列表和可选搜索词 → 没有搜索词就不动;有搜索词就按 thread_id 查标题表,只保留标题包含搜索词的项 → 出来原列表被缩小。
调用关系:list_threads_from_files_desc 和 list_threads_from_files_asc 在文件兜底搜索时调用它。
调用图:调用 1 个内部函数(find_thread_names_by_ids);被 2 处调用(list_threads_from_files_asc, list_threads_from_files_desc)。
thread_item_sort_key1320–1334 ↗
fn thread_item_sort_key(
item: &ThreadItem,
sort_key: ThreadSortKey,
) -> Option<(OffsetDateTime, uuid::Uuid)>
作用:为一个线程条目算出可排序的时间键。它同时带上文件名里的 UUID,帮助排序稳定。
数据流:进去 ThreadItem 和排序依据 → 它从文件名解析创建时间和 ID;如果按更新时间排序,就读取条目里的 updated_at 或 created_at → 出来可选的时间加 UUID。
调用关系:cursor_from_thread_item 用它生成分页游标;升序列表也用同样的思路排序。
调用图:调用 1 个内部函数(parse_timestamp_uuid_from_filename);被 1 处调用(cursor_from_thread_item);外部调用 1 个(parse)。
cursor_from_thread_item1336–1340 ↗
fn cursor_from_thread_item(item: &ThreadItem, sort_key: ThreadSortKey) -> Option<Cursor>
作用:根据一个线程条目生成“下一页从哪里开始”的游标。游标可以理解成书签。
数据流:进去 ThreadItem 和排序键 → 它先算排序时间,再格式化为标准时间字符串并解析成 Cursor → 出来可选游标。
调用关系:文件列表分页需要 next_cursor 时会调用它,尤其在 list_threads_from_files_desc 和 list_threads_from_files_asc 的搜索/截断逻辑中使用。
调用图:调用 2 个内部函数(parse_cursor, thread_item_sort_key)。
precompute_log_file_info1353–1383 ↗
fn precompute_log_file_info(
config: &impl RolloutConfigView,
conversation_id: ThreadId,
) -> std::io::Result<LogFileInfo>
作用:为新会话提前算好 rollout 文件应该放在哪里、叫什么名字。文件名里包含时间和会话 ID,方便人和程序定位。
数据流:进去配置和会话 ID → 它取本地时间,拼出 sessions/年/月/日 目录和 rollout-时间-ID.jsonl 文件名 → 出来 LogFileInfo,包含完整路径、会话 ID 和开始时间。
调用关系:RolloutRecorder::new 在创建新会话录制器时调用它;真正建目录和开文件会推迟到 open_log_file。
调用图:被 1 处调用(new);外部调用 4 个(now_local, format!, format_description!, codex_home)。
open_log_file1385–1398 ↗
fn open_log_file(path: &Path) -> std::io::Result<File>
作用:打开可追加写入的 rollout 文件,必要时创建父目录和文件。它还会先把压缩 rollout 转成可追加的普通文件。
数据流:进去一个路径 → 它处理压缩文件、确认父目录存在、创建目录,然后用追加模式打开文件 → 出来标准文件句柄,失败则返回 IO 错误。
调用关系:RolloutWriterState::ensure_writer_open 在真正需要写入或重试时调用它。
调用图:调用 1 个内部函数(materialize_rollout_for_append_blocking);被 1 处调用(ensure_writer_open);外部调用 5 个(parent, other, format!, create_dir_all, new)。
RolloutWriterState::new1416–1432 ↗
fn new(
file: Option<tokio::fs::File>,
deferred_log_file_info: Option<LogFileInfo>,
meta: Option<SessionMeta>,
cwd: PathBuf,
rollout_path: PathBuf,
) -> Sel
作用:创建后台写入器自己的可变状态。这里保存打开的文件、还没写的记录、待写的会话元数据和恢复用信息。
数据流:进去可选文件、可选延迟建文件信息、会话元数据、当前目录和 rollout 路径 → 它把文件包装成 JsonlWriter,把 pending_items 初始化为空 → 出来 RolloutWriterState。
调用关系:rollout_writer 启动后马上调用它,之后所有命令都围绕这个状态对象工作。
调用图:被 2 处调用(rollout_writer, writer_state_retries_write_error_before_reporting_flush_success);外部调用 1 个(new)。
RolloutWriterState::add_items1434–1436 ↗
fn add_items(&mut self, items: Vec<RolloutItem>)
作用:把新收到的记录追加到待写队列。只有写成功后,这些记录才会从队列里移除。
数据流:进去一批 RolloutItem → 它把它们接到 pending_items 后面 → 不返回业务结果,只改变内存队列。
调用关系:rollout_writer 收到 AddItems 命令时调用它,随后可能调用 flush_if_materialized 尝试写盘。
RolloutWriterState::flush_if_materialized1438–1445 ↗
async fn flush_if_materialized(&mut self)
作用:如果文件已经创建,就顺手把待写记录刷出去;如果还处于延迟创建状态,就先不建文件。这样新会话不会因为只有内存记录就立刻落盘。
数据流:进去没有额外参数 → 它先检查是否 deferred;不是 deferred 就调用 flush,失败时进入恢复模式 → 出来没有结果,可能改变 writer 状态。
调用关系:rollout_writer 收到 AddItems 后调用它,用于“文件已存在就自动写,未持久化前先缓存”。
调用图:调用 3 个内部函数(enter_recovery_mode, flush, is_deferred)。
RolloutWriterState::persist1447–1449 ↗
async fn persist(&mut self) -> std::io::Result<()>
作用:执行一次强制持久化,把文件打开并写完待写记录。它会使用带重试的写入流程。
数据流:进去没有额外参数 → 它调用 write_pending_with_recovery,操作名是 persist → 出来写入成功或最终错误。
调用关系:rollout_writer 收到 Persist 命令时调用它。
调用图:调用 1 个内部函数(write_pending_with_recovery)。
RolloutWriterState::flush1451–1456 ↗
async fn flush(&mut self) -> std::io::Result<()>
作用:把当前待写记录刷到文件;如果还没创建文件且没有待写内容,就直接成功。它是常规“确认写完”动作。
数据流:进去没有额外参数 → 它判断延迟状态和待写队列,必要时调用 write_pending_with_recovery → 出来成功或错误。
调用关系:RolloutRecorder::flush 经 rollout_writer 触发它;flush_if_materialized 也会调用它。
调用图:调用 2 个内部函数(is_deferred, write_pending_with_recovery);被 1 处调用(flush_if_materialized)。
RolloutWriterState::shutdown1458–1463 ↗
async fn shutdown(&mut self) -> std::io::Result<()>
作用:关闭前写完所有待写记录。没有内容且还没建文件时,它不会为了关闭而创建空文件。
数据流:进去没有额外参数 → 它检查是否无需写入;否则调用 write_pending_with_recovery → 出来成功或错误。
调用关系:rollout_writer 收到 Shutdown 命令时调用它;成功后后台循环才会退出。
调用图:调用 2 个内部函数(is_deferred, write_pending_with_recovery)。
RolloutWriterState::write_pending_with_recovery1465–1490 ↗
async fn write_pending_with_recovery(&mut self, operation: &str) -> std::io::Result<()>
作用:带恢复能力地写待写记录:先试一次,失败就丢掉坏文件句柄、重开再试一次。它防止短暂文件错误导致缓存记录立刻丢失。
数据流:进去操作名,比如 flush 或 shutdown → 它调用 write_pending_once;第一次失败会进入恢复模式并重试;第二次还失败才返回错误 → 成功时清掉上次错误记录。
调用关系:persist、flush、shutdown 都通过它写盘;失败处理交给 enter_recovery_mode。
调用图:调用 2 个内部函数(enter_recovery_mode, write_pending_once);被 3 处调用(flush, persist, shutdown);外部调用 1 个(warn!)。
RolloutWriterState::is_deferred1492–1494 ↗
fn is_deferred(&self) -> bool
作用:判断当前是否处于“文件还没真正打开,但有预计算文件信息”的延迟创建状态。
数据流:进去没有参数 → 它检查 writer 是否为空且 deferred_log_file_info 是否存在 → 出来 true 或 false。
调用关系:flush_if_materialized、flush、shutdown 用它决定要不要现在创建文件。
调用图:被 3 处调用(flush, flush_if_materialized, shutdown)。
RolloutWriterState::enter_recovery_mode1496–1509 ↗
fn enter_recovery_mode(&mut self, err: &IoError)
作用:让写入器进入恢复模式:记录错误、关闭当前文件句柄,但保留还没写完的记录。下次写入会重新打开文件再试。
数据流:进去一个 IO 错误 → 它记录错误文字,必要时打日志,然后把 writer 设为空 → 出来没有返回值,但状态变成可重开的状态。
调用关系:flush_if_materialized 和 write_pending_with_recovery 在写盘失败时调用它。
调用图:被 2 处调用(flush_if_materialized, write_pending_with_recovery);外部调用 2 个(to_string, error!)。
RolloutWriterState::ensure_writer_open1511–1527 ↗
async fn ensure_writer_open(&mut self) -> std::io::Result<()>
作用:确保后台写入器手里有一个打开的文件。没有的话,就按延迟路径或已有路径打开。
数据流:进去没有额外参数 → 如果 writer 已存在就直接成功;否则调用 open_log_file 打开文件并包装成 JsonlWriter,同时清掉 deferred 信息 → 出来成功或打开文件错误。
调用关系:write_pending_once 每次真正写入前都会调用它。
调用图:调用 1 个内部函数(open_log_file);被 1 处调用(write_pending_once);外部调用 2 个(as_path, from_std)。
RolloutWriterState::write_session_meta_if_needed1529–1536 ↗
async fn write_session_meta_if_needed(&mut self) -> std::io::Result<()>
作用:如果新会话的 SessionMeta 还没写,就先写这条会话元数据。它通常是 rollout 文件的开头信息。
数据流:进去没有额外参数 → 如果 meta 存在,就调用 write_session_meta 写入,并把 meta 清空避免重复写 → 出来成功或写入错误。
调用关系:write_pending_once 在写普通记录前调用它,保证会话身份、目录、来源等信息先落盘。
调用图:调用 1 个内部函数(write_session_meta);被 1 处调用(write_pending_once)。
RolloutWriterState::write_pending_once1538–1548 ↗
async fn write_pending_once(&mut self) -> std::io::Result<()>
作用:执行一次不带重试的完整写入:打开文件、写会话元数据、写待写记录、最后刷新文件。
数据流:进去没有额外参数 → 它确保文件打开,必要时写 SessionMeta,再写 pending_items,最后 flush 文件 → 出来成功或第一个遇到的 IO 错误。
调用关系:write_pending_with_recovery 调用它;出错后的恢复和重试不在这里做。
调用图:调用 3 个内部函数(ensure_writer_open, write_pending_items_once, write_session_meta_if_needed);被 1 处调用(write_pending_with_recovery)。
RolloutWriterState::write_pending_items_once1550–1570 ↗
async fn write_pending_items_once(&mut self) -> std::io::Result<()>
作用:把待写队列里的记录按顺序写进 JSONL 文件。只要某条失败,后面的会保留在队列里等待下次重试。
数据流:进去没有额外参数,但读取 self.pending_items 和 writer → 它逐条调用 JsonlWriter 写入,统计成功数量;成功写过的从队列头部删除 → 出来成功或失败错误。
调用关系:write_pending_once 在会话元数据写完后调用它。
调用图:被 1 处调用(write_pending_once);外部调用 1 个(other)。
rollout_writer1573–1609 ↗
async fn rollout_writer(
file: Option<tokio::fs::File>,
deferred_log_file_info: Option<LogFileInfo>,
mut rx: mpsc::Receiver<RolloutCmd>,
meta: Option<SessionMeta>,
cwd: PathBuf,
作用:后台写入任务的主循环。它像一个专门负责写日志的工人,前台通过队列给它派活。
数据流:进去可选文件、延迟文件信息、命令接收端、元数据、cwd 和路径 → 它创建 RolloutWriterState,然后不断接收 AddItems、Persist、Flush、Shutdown 命令并执行 → Shutdown 成功或通道关闭后结束。
调用关系:RolloutRecorder::new 会把它作为 Tokio 异步任务启动;它把具体状态变化交给 RolloutWriterState 的方法。
write_session_meta1611–1635 ↗
async fn write_session_meta(
mut writer: Option<&mut JsonlWriter>,
session_meta: SessionMeta,
cwd: &Path,
) -> std::io::Result<()>
作用:写入会话的开头元数据,并尽量附带当前 Git 仓库信息。Git 信息包括分支、提交号和远程地址,方便以后知道这次会话发生在哪个代码版本上。
数据流:进去可选 JsonlWriter、SessionMeta 和当前目录 → 它判断 cwd 是否在 Git 仓库里,能收集就转成协议里的 GitInfo,然后包装成 SessionMeta 记录写入 → 出来成功或写入错误。
调用关系:RolloutWriterState::write_session_meta_if_needed 调用它,确保新建 rollout 的第一批内容包含会话身份信息。
调用图:被 1 处调用(write_session_meta_if_needed);外部调用 3 个(collect_git_info, get_git_repo_root, SessionMeta)。
append_rollout_item_to_path1642–1653 ↗
async fn append_rollout_item_to_path(
rollout_path: &Path,
item: &RolloutItem,
) -> std::io::Result<()>
作用:直接往某个已有 rollout 文件末尾追加一条记录。它用于更新未加载线程的元数据;活跃会话不该用它,以免打乱实时记录顺序。
数据流:进去 rollout 路径和一条 RolloutItem → 它先把可能压缩的文件转成可追加文件,打开为追加模式,再用 JsonlWriter 写一行 → 出来成功或 IO 错误。
调用关系:独立的元数据更新流程会用它;实时写入应走 RolloutRecorder::record_canonical_items。
调用图:调用 1 个内部函数(materialize_rollout_for_append);外部调用 1 个(new)。
JsonlWriter::write_rollout_item1667–1680 ↗
async fn write_rollout_item(&mut self, rollout_item: &RolloutItem) -> std::io::Result<()>
作用:把一个 RolloutItem 包成带当前时间戳的 JSONL 行。每行都有写入时间,方便以后查看事件顺序。
数据流:进去一条 RolloutItem → 它生成 UTC 时间字符串,构造 RolloutLineRef,然后交给 write_line 序列化写入 → 出来成功或序列化/写文件错误。
调用关系:RolloutWriterState::write_pending_items_once、write_session_meta 和 append_rollout_item_to_path 都通过它写单条记录。
调用图:调用 1 个内部函数(write_line);外部调用 2 个(now_utc, format_description!)。
JsonlWriter::write_line1681–1687 ↗
async fn write_line(&mut self, item: &impl serde::Serialize) -> std::io::Result<()>
作用:把一个可序列化对象写成一行 JSON,并立刻刷新到文件。JSONL 的意思就是“一行一个 JSON”。
数据流:进去任意可序列化对象 → 它转成 JSON 字符串,加换行,写入文件,再 flush → 出来成功或错误。
调用关系:JsonlWriter::write_rollout_item 用它完成最终的磁盘写入。
调用图:被 1 处调用(write_rollout_item);外部调用 3 个(flush, write_all, to_string)。
ThreadsPage::from1691–1703 ↗
fn from(db_page: codex_state::ThreadsPage) -> Self
作用:把状态库返回的线程分页转换成本模块列表接口使用的分页格式。这样上层不需要关心数据来自 SQLite 还是文件。
数据流:进去 codex_state::ThreadsPage → 它逐条把 ThreadMetadata 转成 ThreadItem,并把数据库 next_anchor 转成通用 cursor → 出来 rollout 列表使用的 ThreadsPage。
调用关系:list_threads_with_db_fallback 在采用数据库结果时会通过这个转换返回。
thread_item_from_state_metadata1706–1729 ↗
fn thread_item_from_state_metadata(item: codex_state::ThreadMetadata) -> ThreadItem
作用:把状态数据库里的一条线程元数据转换成列表展示用的 ThreadItem。它会整理来源、模型、时间、Git 信息等字段。
数据流:进去 ThreadMetadata → 它复制路径、ID、标题预览、cwd、Git 字段等,并把来源字符串解析成 SessionSource,时间转成 RFC3339 字符串 → 出来 ThreadItem。
调用关系:ThreadsPage::from 和 fill_missing_thread_item_metadata_from_state_db 都会用它把数据库记录变成列表条目。
调用图:被 1 处调用(fill_missing_thread_item_metadata_from_state_db);外部调用 1 个(from_str)。
select_resume_path1731–1754 ↗
async fn select_resume_path(
page: &ThreadsPage,
filter_cwd: Option<&Path>,
default_provider: &str,
) -> Option<PathBuf>
作用:从文件扫描得到的一页线程里选一个适合恢复的 rollout 路径。没有目录过滤时直接选第一页第一项。
数据流:进去 ThreadsPage、可选 cwd 过滤和默认模型提供方 → 如果有 cwd,就逐个检查候选是否匹配;没有 cwd 就返回第一项路径 → 出来可选 PathBuf。
调用关系:RolloutRecorder::find_latest_thread_path 文件系统兜底阶段会调用它;目录匹配交给 resume_candidate_matches_cwd。
调用图:调用 1 个内部函数(resume_candidate_matches_cwd);被 1 处调用(find_latest_thread_path)。
resume_candidate_matches_cwd1756–1782 ↗
async fn resume_candidate_matches_cwd(
rollout_path: &Path,
cached_cwd: Option<&Path>,
cwd: &Path,
default_provider: &str,
) -> bool
作用:判断某个候选会话是否属于指定工作目录。它会先信缓存目录,不够再读 rollout 内容,最后再提取元数据兜底。
数据流:进去 rollout 路径、可选缓存 cwd、目标 cwd 和默认提供方 → 它先比较缓存 cwd;不匹配就读 rollout_items 找最后的 TurnContext cwd;还不行就从文件提取元数据 → 出来 true 或 false。
调用关系:select_resume_path 和 select_resume_path_from_db_page 都用它过滤恢复候选;它会调用 RolloutRecorder::load_rollout_items 和 metadata::extract_metadata_from_rollout。
调用图:调用 3 个内部函数(extract_metadata_from_rollout, load_rollout_items, cwd_matches);被 2 处调用(select_resume_path, select_resume_path_from_db_page)。
select_resume_path_from_db_page1784–1807 ↗
async fn select_resume_path_from_db_page(
page: &codex_state::ThreadsPage,
filter_cwd: Option<&Path>,
default_provider: &str,
) -> Option<PathBuf>
作用:从数据库返回的一页线程里选一个可恢复路径。它和文件版本类似,但数据库条目本身带 cwd 缓存。
数据流:进去 codex_state 的 ThreadsPage、可选 cwd 和默认提供方 → 有 cwd 时逐个用 resume_candidate_matches_cwd 验证;没有 cwd 时取第一项路径 → 出来可选 PathBuf。
调用关系:RolloutRecorder::find_latest_thread_path 优先查数据库时调用它。
调用图:调用 1 个内部函数(resume_candidate_matches_cwd);被 1 处调用(find_latest_thread_path)。
cwd_matches1809–1811 ↗
fn cwd_matches(session_cwd: &Path, cwd: &Path) -> bool
作用:比较两个工作目录是否可以认为是同一个目录。它会做路径标准化,避免因为写法不同而误判,比如符号链接或斜杠差异。
数据流:进去两个路径 → 它调用路径工具做规范化后的比较 → 出来 true 或 false。
调用关系:resume_candidate_matches_cwd 在判断恢复候选是否属于当前目录时调用它。
调用图:被 1 处调用(resume_candidate_matches_cwd);外部调用 1 个(paths_match_after_normalization)。
线程存储契约与测试后端
这些文件建立与存储无关的线程持久化 API、共享错误,以及用于测试和调试的内存实现。
thread-store/src/error.rs源码 ↗
线程存储可以理解成一个保存和读取对话线程的仓库。访问仓库时,可能会遇到很多情况:要找的线程不存在,请求内容写错了,当前状态不允许这么改,某个功能还没实现,或者仓库内部真的出故障了。这个文件就是把这些情况统一收拢成一个错误类型 ThreadStoreError。这样其他代码只要返回 ThreadStoreResult<T>,就等于在说:“成功时给你 T,失败时给你一个标准化的线程存储错误。”这很重要,因为如果每个实现都随便报错,上层代码就很难判断该提示用户、重试、还是直接终止。这里还用 thiserror 帮每种错误准备了清楚的文字说明,比如“thread xxx not found”,让日志和界面提示都更容易看懂。
thread-store/src/lib.rs源码 ↗
这个库要解决的问题是:应用里有很多“线程”或“对话记录”,它们可能存在内存里、本地文件里,未来也可能存在别的地方。外部代码不应该到处知道这些存储细节,否则一换存储方式就会牵一发动全身。这个文件就像商店的前台,把真正干活的模块藏在后面,只把稳定好用的名字公开出来。比如 ThreadStore 是统一的存取接口,LocalThreadStore 是本地文件版,InMemoryThreadStore 是内存版,LiveThread 表示正在使用中的线程;各种 Params 类型表示“创建、读取、追加、搜索时要带哪些信息”。这样别的代码只需要从这里拿这些公开类型,就能使用线程存储能力,而不用直接摸到内部实现。
thread-store/src/store.rs源码 ↗
这个文件像是在给“对话线程仓库”写一份菜单:能新建线程、继续旧线程、追加消息、把内存里的内容落盘、读取历史、列出线程、更新元数据、归档和删除。这里的“线程”可以理解成一段持续的对话或任务记录;“持久化”就是把东西真正保存下来,避免程序退出后丢失。核心是 ThreadStore 这个 trait。trait 可以理解成一份能力清单:谁想当线程仓库,就必须提供这些能力。文件还定义了 ThreadStoreFuture,意思是这些操作通常不是马上完成,而是异步完成;比如写磁盘或查数据库需要等。值得注意的是,搜索线程、列出轮次、列出条目这几个功能给了默认实现:如果具体仓库没支持,就会返回“不支持该操作”的错误,而不是假装成功。
ThreadStore::search_threads85–94 ↗
fn search_threads(
&self,
_params: SearchThreadsParams,
) -> ThreadStoreFuture<'_, ThreadSearchPage>
作用:这是“搜索线程”的默认做法。它不是直接搜索,而是在具体存储没有自己实现搜索功能时,明确告诉调用者:这个仓库不支持搜索。
数据流:进去的是搜索参数,但默认实现不会使用这些条件。它把一个异步任务包装起来,任务执行后返回 ThreadStoreError::Unsupported,说明不支持 “thread/search” 这个操作。出来的不是搜索结果页,而是一个失败结果;存储内容不会被改动。
调用关系:当上层代码通过 ThreadStore 这个统一接口请求搜索,而实际的存储实现没有重写这个函数时,就会走到这里。它只把错误放进异步返回值里,借助 pin 把这个异步任务固定并交还给调用者。
调用图:外部调用 1 个(pin)。
ThreadStore::list_turns97–103 ↗
fn list_turns(&self, _params: ListTurnsParams) -> ThreadStoreFuture<'_, TurnPage>
作用:这是“列出某个线程里的轮次”的默认做法。这里的“轮次”可以理解成对话中的一段来回;如果具体仓库没实现这个能力,它会直接返回不支持。
数据流:进去的是列出轮次所需的参数,但默认实现不会读取这些参数。它创建一个异步任务,任务完成时返回 ThreadStoreError::Unsupported,标明不支持 “list_turns”。出来的是错误结果,不会返回轮次列表,也不会修改任何线程数据。
调用关系:当调用者想按轮次查看线程内容,但当前存储后端没有提供自己的 list_turns 实现时,这个默认函数就接手。它不再把工作交给真正的查询逻辑,只是用 pin 包好一个异步错误结果返回。
调用图:外部调用 1 个(pin)。
ThreadStore::list_items106–112 ↗
fn list_items(&self, _params: ListItemsParams) -> ThreadStoreFuture<'_, ItemPage>
作用:这是“列出某个轮次里已保存条目”的默认做法。这里的“条目”可以理解成实际保存下来的消息、事件或记录;如果仓库不支持细粒度查看,就会返回不支持。
数据流:进去的是列出条目的参数,但默认实现不会使用它们。它生成一个异步任务,任务执行后返回 ThreadStoreError::Unsupported,说明 “list_items” 这个操作不可用。出来的是错误,而不是条目分页;现有数据保持不变。
调用关系:当上层代码通过统一的 ThreadStore 接口请求查看某个轮次里的具体条目,而具体存储没有覆盖这个方法时,就会用这里的默认实现。它只负责把“不支持”这个答复包装成异步结果,并通过 pin 返回给调用者。
调用图:外部调用 1 个(pin)。
thread-store/src/in_memory.rs源码 ↗
真实的会话存储可能要访问磁盘、数据库或远程 gRPC 服务(可以理解成另一台服务机器上的接口),测试时这样太重也太慢。这个文件提供了 InMemoryThreadStore,把会话、历史记录、元数据更新、rollout 路径映射都放在 HashMap(一张按 key 查值的表)里。它还会记录每种操作被调用了几次,方便测试确认“代码真的走到了这里”。内部用 tokio 的互斥锁(一把异步锁,防止多个任务同时改同一份数据)保护状态。文件还提供按 id 共享同一个内存 store 的全局表,像给不同测试准备不同编号的临时储物柜。要注意:它不是完整数据库,分页 turns/items 默认不支持;有些操作,比如归档,只记调用次数,不真的改变归档状态。
stores40–42 ↗
fn stores() -> &'static Mutex<HashMap<String, Arc<InMemoryThreadStore>>>
作用:拿到全局的内存 store 总表。这个总表按字符串 id 保存多个共享的 InMemoryThreadStore,方便不同测试用同一个名字找到同一个假存储。
数据流:进去没有普通参数 → 它查看全局 OnceLock(只初始化一次的容器),如果还没建表就新建一个带互斥锁的 HashMap → 出来一个全局可用的加锁总表引用。
调用关系:stores_guard 会调用它。stores 只负责找到或创建总表,真正加锁和处理锁出错的事情交给 stores_guard。
调用图:被 1 处调用(stores_guard)。
tests::default_turn_pagination_methods_return_unsupported57–96 ↗
async fn default_turn_pagination_methods_return_unsupported()
作用:这是一个测试,确认内存 store 默认不支持按 turn 或 item 分页读取。它防止别人误以为这个假存储已经实现了完整分页能力。
数据流:进去没有外部输入 → 它创建默认 InMemoryThreadStore 和一个线程 id,然后分别调用 list_turns、list_items → 出来不是成功结果,而是确认返回 Unsupported 错误。
调用关系:它直接用默认 store 做检查。这个测试守住接口边界:如果以后有人改了默认行为,这里会提醒大家。
tests::list_threads_filters_by_parent_thread_id99–158 ↗
async fn list_threads_filters_by_parent_thread_id()
作用:这是一个测试,确认列出会话时可以只拿某个父会话下面的子会话。它避免“把不相关会话也列出来”的错误。
数据流:进去没有外部输入 → 它创建一个父 id、一个子线程、一个无关线程,把两条线程写进内存 store → 调用 list_threads 并指定 parent_thread_id → 出来只应该包含那个子线程。
调用关系:它通过 ThreadStore 的 list_threads 接口验证过滤行为。这个测试对应文件里 list_threads 方法中按 parent_thread_id 保留项目的那段逻辑。
调用图:调用 3 个内部函数(default, default, from_string);外部调用 4 个(new, assert_eq!, default, list_threads)。
stores_guard161–166 ↗
fn stores_guard() -> MutexGuard<'static, HashMap<String, Arc<InMemoryThreadStore>>>
作用:给全局 store 总表上锁,并返回能安全读写它的钥匙。这样多个测试同时跑时,不会同时改坏同一张表。
数据流:进去没有普通参数 → 它先通过 stores 找到总表,再尝试加 Mutex 锁;如果锁曾经被异常中断弄“中毒”,它也会取回里面的数据继续用 → 出来一个 MutexGuard,也就是持锁访问总表的临时权限。
调用关系:InMemoryThreadStore::for_id 和 InMemoryThreadStore::remove_id 都靠它进入全局表。它夹在 stores 和具体增删操作之间,保证访问安全。
InMemoryThreadStore::for_id211–218 ↗
fn for_id(id: impl Into<String>) -> Arc<Self>
作用:按 id 获取一个共享的内存 store;如果这个 id 还没有对应 store,就现场创建一个。测试可以用它模拟“配置里指定了某个远程存储”。
数据流:进去一个可转成字符串的 id → 它把 id 转成字符串,锁住全局表,查这个 id 是否已有 store;没有就放入一个默认的新 store → 出来一个 Arc 包着的 store,Arc 是可共享引用计数指针,方便多处共用同一个对象。
调用关系:很多测试和配置创建逻辑会调用它,比如读取无线程路径的 store、冷启动恢复、删除线程、列线程等场景。它会先找 stores_guard 拿到全局表的写入权限。
调用图:调用 1 个内部函数(stores_guard);被 8 处调用(get_conversation_summary_by_thread_id_reads_pathless_store_thread, cold_thread_resume_reuses_non_local_history_probe, thread_delete_with_non_local_thread_store_does_not_create_local_persistence, thread_list_includes_store_thread_without_rollout_path, thread_read_loaded_include_turns_reads_store_history_without_rollout_path, thread_turns_list_reads_store_history_without_rollout_path, thread_unarchive_preserves_pathless_store_metadata, thread_store_from_config);外部调用 1 个(into)。
InMemoryThreadStore::remove_id221–223 ↗
fn remove_id(id: &str) -> Option<Arc<Self>>
作用:从全局表里移除某个 id 对应的共享内存 store。测试结束时可以用它清理现场,避免一个测试留下的数据影响下一个测试。
数据流:进去一个字符串 id → 它锁住全局表,然后删除这个 id 的条目 → 出来可能是被删掉的 store;如果原来没有这个 id,就出来 None。
调用关系:它通过 stores_guard 操作全局表。调用图里它常在 drop 清理阶段被调用,也就是测试辅助对象销毁时顺手打扫。
调用图:调用 1 个内部函数(stores_guard);被 4 处调用(drop, drop, drop, drop)。
InMemoryThreadStore::calls226–228 ↗
async fn calls(&self) -> InMemoryThreadStoreCalls
作用:读取这个内存 store 到目前为止各类操作被调用了多少次。测试用它确认某个流程有没有真的创建、追加、读取或删除线程。
数据流:进去是当前 store 自己 → 它锁住内部状态,复制 calls 计数快照 → 出来一份 InMemoryThreadStoreCalls,不改动原数据。
调用关系:它通常被测试断言使用。它不参与业务动作,只像计数器读数一样告诉外面“刚才发生过哪些操作”。
InMemoryThreadStore::as_any390–392 ↗
fn as_any(&self) -> &dyn std::any::Any
作用:把这个 store 暴露成 Any 类型,方便需要时做类型识别或向下转换。简单说,就是让外层代码能确认“这到底是不是 InMemoryThreadStore”。
数据流:进去是当前 store 自己 → 它不改任何状态,只返回一个通用类型视角的引用 → 出来的是 &dyn Any。
调用关系:这是 ThreadStore 接口的一部分。外层如果拿到的是抽象的 ThreadStore,可以通过这个入口尝试识别具体实现。
InMemoryThreadStore::create_thread394–396 ↗
fn create_thread(&self, params: CreateThreadParams) -> ThreadStoreFuture<'_, ()>
作用:在内存里创建一条新会话,并给它放入一条基础的 SessionMeta 记录。SessionMeta 可以理解成会话的身份证,记录 id、来源、模型提供方、父子关系等基本信息。
数据流:进去 CreateThreadParams,里面有线程 id、来源、父线程、工作目录、模型提供方、基础指令等 → 它加锁,创建调用次数加一,把这些信息整理成 SessionMeta,再作为第一条历史项放进 histories,同时保存创建参数 → 出来成功或错误结果;正常情况下内存状态多了一条线程和一条初始历史。
调用关系:测试辅助函数 seed_pathless_store_thread 会用它先种一条线程。作为 ThreadStore 的创建入口时,外层调用 create_thread 会被转到这里执行。
调用图:调用 1 个内部函数(default);被 1 处调用(seed_pathless_store_thread);外部调用 3 个(pin, matches!, SessionMeta)。
InMemoryThreadStore::resume_thread398–400 ↗
fn resume_thread(&self, params: ResumeThreadParams) -> ThreadStoreFuture<'_, ()>
作用:把一条已有会话恢复到内存 store 里。它可以带着历史记录恢复,也可以只登记一个空历史,还能记住 rollout 文件路径到线程 id 的对应关系。
数据流:进去 ResumeThreadParams,包含线程 id、可选历史、可选 rollout_path → 它加锁并把 resume_thread 计数加一;如果带历史就覆盖保存,没有历史就确保有一个空列表;如果带路径就保存路径映射 → 出来成功结果,同时内存里有可读的线程历史入口或路径入口。
调用关系:这是 ThreadStore 恢复线程接口的内存版。外层在恢复会话时通过接口调用它,之后 read_thread_by_rollout_path 就可以靠路径找到线程。
调用图:外部调用 1 个(pin)。
InMemoryThreadStore::append_items402–404 ↗
fn append_items(&self, params: AppendThreadItemsParams) -> ThreadStoreFuture<'_, ()>
作用:给某条会话追加历史项,比如对话中的新消息或系统记录。它会先过滤成真正需要持久保存的 rollout 项,避免把不该保存的临时内容塞进去。
数据流:进去 AppendThreadItemsParams,包含线程 id 和一批 items → 它先调用 persisted_rollout_items 做规范化和过滤;如果过滤后为空,直接成功返回且不增加调用次数;否则加锁、append_items 计数加一,把这些历史项追加到该线程的历史列表 → 出来成功结果,内存历史变长。
调用关系:seed_pathless_store_thread 会用它给测试线程补历史。它依赖 codex_rollout 的 persisted_rollout_items 决定哪些项目才算应该保存。
调用图:被 1 处调用(seed_pathless_store_thread);外部调用 2 个(pin, persisted_rollout_items)。
InMemoryThreadStore::persist_thread406–411 ↗
fn persist_thread(&self, _thread_id: ThreadId) -> ThreadStoreFuture<'_, ()>
作用:模拟“把线程持久化保存”的动作。因为这里本来就只存在内存里,所以它不真的写磁盘或远程服务,只记录这件事被要求过。
数据流:进去一个线程 id,但当前实现不使用它 → 它加锁,把 persist_thread 计数加一 → 出来成功结果,除了计数外不改线程内容。
调用关系:这是 ThreadStore 接口要求的方法。外层流程如果在某个阶段要求保存线程,调用到内存 store 时就只会留下调用计数,方便测试检查。
调用图:外部调用 1 个(pin)。
InMemoryThreadStore::flush_thread413–418 ↗
fn flush_thread(&self, _thread_id: ThreadId) -> ThreadStoreFuture<'_, ()>
作用:模拟“刷新线程数据”的动作。真实存储里这可能表示把缓冲区内容立刻写出去;内存版只记一次调用。
数据流:进去一个线程 id,但这里不读取具体线程 → 它加锁,把 flush_thread 计数加一 → 出来成功结果,线程数据本身不变。
调用关系:这是 ThreadStore 接口的一部分。外层要求 flush 时会走到这里,测试可以通过 calls 看它是否发生过。
调用图:外部调用 1 个(pin)。
InMemoryThreadStore::shutdown_thread420–425 ↗
fn shutdown_thread(&self, _thread_id: ThreadId) -> ThreadStoreFuture<'_, ()>
作用:模拟关闭某条线程的存储资源。真实实现可能要断开连接或收尾;这里没有资源要关,只记录调用次数。
数据流:进去一个线程 id → 它加锁,把 shutdown_thread 计数加一 → 出来成功结果,不删除也不修改线程内容。
调用关系:外层在线程结束或程序收尾时可能调用这个 ThreadStore 方法。内存版让流程能正常跑完,同时留下可检查的计数。
调用图:外部调用 1 个(pin)。
InMemoryThreadStore::discard_thread427–432 ↗
fn discard_thread(&self, _thread_id: ThreadId) -> ThreadStoreFuture<'_, ()>
作用:模拟丢弃某条线程的未保存内容。内存版没有真正的未保存缓冲区,所以只把 discard_thread 计数加一。
数据流:进去一个线程 id → 它加锁并增加 discard_thread 计数 → 出来成功结果,保存的线程和历史不变。
调用关系:这是 ThreadStore 接口的占位实现。外层放弃某次线程状态时会调用它,测试可以确认这条路径有没有走到。
调用图:外部调用 1 个(pin)。
InMemoryThreadStore::load_history434–439 ↗
fn load_history(
&self,
params: LoadThreadHistoryParams,
) -> ThreadStoreFuture<'_, StoredThreadHistory>
作用:读取某条线程的完整历史记录。测试用它验证之前创建或追加进去的消息是否能被重新取出来。
数据流:进去 LoadThreadHistoryParams,里面有线程 id → 它加锁,load_history 计数加一,到 histories 表里找这条线程;找到就复制历史项并包装成 StoredThreadHistory,找不到就返回 ThreadNotFound 错误 → 出来历史记录或未找到错误。
调用关系:这是 ThreadStore 的历史读取入口。外层需要恢复上下文、列出对话内容时会调用它。
调用图:外部调用 1 个(pin)。
InMemoryThreadStore::read_thread441–443 ↗
fn read_thread(&self, params: ReadThreadParams) -> ThreadStoreFuture<'_, StoredThread>
作用:读取一条线程的概要信息,也可以按要求带上历史。它把内存里分散保存的创建参数、元数据更新、名称和历史拼成一个 StoredThread。
数据流:进去 ReadThreadParams,包含线程 id 和 include_history 开关 → 它加锁,read_thread 计数加一;如果要求包含历史,也把 read_thread_with_history 计数加一;然后调用 stored_thread_from_state 组装结果 → 出来 StoredThread,或线程不存在错误。
调用关系:它是普通按 id 读取线程的接口。真正拼对象的细活交给 stored_thread_from_state,这样读取、更新后返回、取消归档后返回等地方能用同一套组装规则。
调用图:调用 1 个内部函数(stored_thread_from_state);外部调用 1 个(pin)。
InMemoryThreadStore::read_thread_by_rollout_path445–452 ↗
fn read_thread_by_rollout_path(
&self,
params: ReadThreadByRolloutPathParams,
) -> ThreadStoreFuture<'_, StoredThread>
作用:通过 rollout 文件路径读取线程。rollout 路径可以理解成某条会话记录文件的位置;这个函数用路径反查线程 id。
数据流:进去 ReadThreadByRolloutPathParams,包含 rollout_path 和是否带历史 → 它加锁,计数加一,到 rollout_paths 表里找这个路径对应的线程 id;找不到就返回 InvalidRequest;找到后调用 stored_thread_from_state 组装线程 → 出来 StoredThread 或错误。
调用关系:resume_thread 保存过路径映射后,这个读取入口才有东西可查。它把“路径变线程 id”的部分自己做掉,再把“线程 id 变完整对象”的部分交给 stored_thread_from_state。
调用图:调用 1 个内部函数(stored_thread_from_state);外部调用 2 个(pin, format!)。
InMemoryThreadStore::list_threads454–463 ↗
fn list_threads(&self, params: ListThreadsParams) -> ThreadStoreFuture<'_, ThreadPage>
作用:列出内存 store 里已经创建的线程。通过 ThreadStore 接口调用时,还能按父线程 id 过滤出某个父会话下面的子会话。
数据流:进去 ListThreadsParams,可能带 parent_thread_id 等条件 → 它先从内存状态里拿所有 created_threads,组装成 StoredThread 列表并按线程 id 字符串排序;如果参数指定了 parent_thread_id,就只保留父 id 匹配的项目 → 出来 ThreadPage,next_cursor 永远是 None,表示没有下一页。
调用关系:测试 list_threads_filters_by_parent_thread_id 会专门验证父线程过滤。它作为 ThreadStore 的列表入口,被上层列会话页面或测试代码调用。
调用图:外部调用 1 个(pin)。
InMemoryThreadStore::update_thread_metadata465–470 ↗
fn update_thread_metadata(
&self,
params: UpdateThreadMetadataParams,
) -> ThreadStoreFuture<'_, StoredThread>
作用:更新某条线程的元数据,比如名称、预览、模型、路径、时间、权限等。它不是直接重写整条线程,而是保存一份补丁。
数据流:进去 UpdateThreadMetadataParams,包含线程 id 和 patch 补丁 → 它加锁,计数加一;如果补丁里带名称,就更新 names 表;然后把补丁合并进 metadata_updates;最后调用 stored_thread_from_state 用新状态组装线程 → 出来更新后的 StoredThread,或线程不存在错误。
调用关系:seed_pathless_store_thread 会用它给测试线程补元数据。它更新完不会自己手写返回对象,而是交给 stored_thread_from_state 保持返回格式一致。
调用图:调用 1 个内部函数(stored_thread_from_state);被 1 处调用(seed_pathless_store_thread);外部调用 1 个(pin)。
InMemoryThreadStore::archive_thread472–477 ↗
fn archive_thread(&self, _params: ArchiveThreadParams) -> ThreadStoreFuture<'_, ()>
作用:模拟归档线程。这里不会真的把线程标成已归档,只记录 archive_thread 被调用过。
数据流:进去 ArchiveThreadParams,但当前实现不读取具体内容 → 它加锁,把 archive_thread 计数加一 → 出来成功结果,线程的 archived_at 仍然不会变化。
调用关系:这是 ThreadStore 归档接口的简化实现。外层流程可以照常调用,测试如果只关心有没有触发归档动作,就看 calls 计数。
调用图:外部调用 1 个(pin)。
InMemoryThreadStore::unarchive_thread479–485 ↗
fn unarchive_thread(&self, params: ArchiveThreadParams) -> ThreadStoreFuture<'_, StoredThread>
作用:模拟取消归档,并返回对应线程。因为内存版没有真正保存归档状态,所以它主要是计数,然后重新读出线程信息。
数据流:进去 ArchiveThreadParams,包含线程 id → 它加锁,把 unarchive_thread 计数加一;然后调用 stored_thread_from_state 组装这个线程,不带历史 → 出来 StoredThread,或线程不存在错误。
调用关系:外层执行取消归档时会走这个 ThreadStore 方法。它把返回线程对象的工作交给 stored_thread_from_state,和 read_thread、update_thread_metadata 使用同一套规则。
调用图:调用 1 个内部函数(stored_thread_from_state);外部调用 1 个(pin)。
InMemoryThreadStore::delete_thread487–489 ↗
fn delete_thread(&self, params: DeleteThreadParams) -> ThreadStoreFuture<'_, ()>
作用:从内存 store 里删除一条线程,以及它相关的历史、名称、元数据补丁和路径映射。测试用它确认删除流程不会留下脏数据。
数据流:进去 DeleteThreadParams,包含线程 id → 它加锁,delete_thread 计数加一;删除 histories、created_threads、names、metadata_updates 里的对应项,并移除所有指向该线程的 rollout_path → 如果原来确实有历史入口就成功,否则返回 ThreadNotFound。
调用关系:这是 ThreadStore 的删除入口。外层删除线程时调用它,它负责把这份内存假数据库里散落的几处记录一起清掉。
调用图:外部调用 1 个(pin)。
stored_thread_from_state492–565 ↗
fn stored_thread_from_state(
state: &InMemoryThreadStoreState,
thread_id: ThreadId,
include_history: bool,
) -> ThreadStoreResult<StoredThread>
作用:把内存里零散的数据拼成一个完整的 StoredThread。它像一个装配工:从创建记录、历史、名称、补丁、路径映射里各拿一块,组装成外面能用的线程对象。
数据流:进去当前状态、线程 id、是否包含历史的开关 → 它先确认线程创建记录存在;再取历史、名称、元数据补丁、rollout 路径;有补丁就优先用补丁里的字段,没有就填默认值或创建时的值;如果 include_history 为真就附上历史 → 出来 StoredThread,或者线程不存在错误。
调用关系:read_thread、read_thread_by_rollout_path、update_thread_metadata、unarchive_thread 在需要返回线程对象时都会用它。这样不同入口返回的字段规则一致,不会各拼各的。
调用图:被 4 处调用(read_thread, read_thread_by_rollout_path, unarchive_thread, update_thread_metadata)。
git_info_from_patch567–580 ↗
fn git_info_from_patch(patch: &ThreadMetadataPatch) -> Option<codex_protocol::protocol::GitInfo>
作用:把元数据补丁里的 Git 信息转换成协议里使用的 GitInfo。Git 是代码版本管理工具;这里关心的是提交号、分支名和仓库地址。
数据流:进去一个 ThreadMetadataPatch → 它查看 patch.git_info;如果没有 Git 信息,或提交号、分支、远程地址全都为空,就返回 None;否则把 sha 转成 GitSha,把分支和仓库地址放进 GitInfo → 出来 Some(GitInfo) 或 None。
调用关系:它是组装线程元数据时的小助手。stored_thread_from_state 在需要把补丁里的 Git 字段放进 StoredThread 时会用到这种转换规则。
本地线程存储基础
这些文件定义由本地文件系统支持的线程存储,以及其具体操作共享的辅助逻辑。
thread-store/src/local/helpers.rs源码 ↗
本地线程存储要面对很多“不整齐”的东西:用户传来的文件路径可能跑到不该访问的目录外;旧版会话日志里可能缺字段;权限配置可能有新旧两种写法;标题可能只是第一句话的重复。这个文件就像仓库门口的分拣台,先把路径、时间、权限、Git 信息、标题这些零散信息检查一遍、补齐一遍,再交给上层读线程、归档、删除、恢复等流程使用。它不会自己完成完整的线程读写流程,而是提供一组小而关键的辅助函数,避免上层代码到处重复写同样的判断,也避免把错误路径、错误权限或重复标题带进系统里。
scoped_rollout_path26–55 ↗
fn scoped_rollout_path(
root: PathBuf,
rollout_path: &Path,
root_name: &str,
) -> ThreadStoreResult<PathBuf>
作用:检查一个会话日志路径是不是确实待在指定的根目录里面。这样可以防止有人传入看起来正常、实际指向别处的路径,误删或移动不该动的文件。
数据流:输入是根目录、要检查的日志路径,以及根目录的名字说明。它先把根目录和日志路径都解析成真实路径,再判断日志路径是否以根目录开头;如果是,就返回解析后的安全路径;如果不是,返回一个“请求无效”的错误。
调用关系:归档、删除、取消归档线程时会先用它把路径把关。它自己只做路径解析和范围检查,后面的 archive_thread、delete_rollout_path、unarchive_thread 才会真的移动或删除文件。
调用图:被 3 处调用(archive_thread, delete_rollout_path, unarchive_thread);外部调用 2 个(format!, canonicalize)。
rollout_path_is_archived57–62 ↗
fn rollout_path_is_archived(codex_home: &Path, path: &Path) -> bool
作用:判断一个会话日志文件是不是已经在“归档区”里。归档区可以理解为旧会话的收纳盒,系统需要知道一个线程现在是正常状态还是归档状态。
数据流:输入是 Codex 的主目录和一个文件路径。它检查这个路径是不是位于归档目录下面,或者路径的任意一段名字就是归档目录名;最后返回 true 或 false。
调用关系:读取历史、读取线程、按日志路径解析线程时都会问它“这个文件是不是归档的”。它不读取文件内容,只给上层流程一个状态判断。
调用图:被 5 处调用(load_history, read_thread, read_thread_from_rollout_path, resolve_rollout_path, stored_thread_from_session_meta);外部调用 3 个(components, join, starts_with)。
matching_rollout_file_name64–92 ↗
fn matching_rollout_file_name(
rollout_path: &Path,
thread_id: ThreadId,
display_path: &Path,
) -> ThreadStoreResult<std::ffi::OsString>
作用:确认一个日志文件名和指定的线程 ID 对得上。这样可以避免用户拿 A 线程的 ID 去操作 B 线程的日志文件。
数据流:输入是日志路径、线程 ID,以及用于显示在错误信息里的路径。它先取出文件名,再检查文件名是否以“线程ID.jsonl”或“线程ID.jsonl.zst”结尾;匹配就返回文件名,不匹配就返回错误。
调用关系:归档、删除、取消归档线程前会调用它做第二道安全检查。它配合 scoped_rollout_path,前者确认路径在安全目录内,后者确认文件名属于目标线程。
调用图:被 3 处调用(archive_thread, delete_rollout_path, unarchive_thread);外部调用 2 个(file_name, format!)。
touch_modified_time94–97 ↗
fn touch_modified_time(path: &Path) -> std::io::Result<()>
作用:把一个文件的“最后修改时间”更新成现在。恢复归档文件时,这能让文件看起来像刚被重新激活过。
数据流:输入是文件路径。它以追加模式打开文件,但不写入内容,只把文件的修改时间设置为当前时间;成功就返回空结果,失败就返回系统输入输出错误。
调用关系:unarchive_thread 在把线程从归档区恢复出来后会用它刷新时间。它只碰文件时间戳,不负责移动文件,也不改文件内容。
调用图:被 1 处调用(unarchive_thread);外部调用 3 个(new, new, now)。
stored_thread_from_rollout_item99–154 ↗
fn stored_thread_from_rollout_item(
item: ThreadItem,
archived: bool,
default_provider: &str,
) -> Option<StoredThread>
作用:把扫描到的一条会话日志摘要,整理成系统内部统一使用的 StoredThread。简单说,就是把“散装日志信息”变成“线程卡片”。
数据流:输入是一条 ThreadItem、它是否已归档,以及默认模型提供方。它会补线程 ID、解析创建和更新时间、整理 Git 信息、选择预览文字、去掉压缩后缀对应的逻辑日志路径,并给缺失字段填默认值;如果连线程 ID 都找不到,就返回 None,否则返回一个 StoredThread。
调用关系:读取某个日志文件、取消归档后重建线程信息,以及对应测试都会调用它。它会把时间解析交给 parse_rfc3339,把 Git 字段整理交给 git_info_from_parts,并使用默认只读权限作为安全兜底。
调用图:调用 3 个内部函数(read_only, git_info_from_parts, parse_rfc3339);被 3 处调用(stored_thread_from_rollout_item_returns_logical_rollout_path, read_thread_from_rollout_path, unarchive_thread);外部调用 1 个(plain_rollout_path)。
permission_profile_from_metadata_value156–163 ↗
fn permission_profile_from_metadata_value(value: &str, cwd: &Path) -> PermissionProfile
作用:把元数据里保存的权限文字还原成 PermissionProfile。PermissionProfile 是权限配置,描述这个线程能不能写文件、能不能联网等。
数据流:输入是元数据字符串和当前工作目录。它先尝试按新版 JSON 权限格式读取;如果失败,再尝试按旧版沙箱策略读取并转换;如果还是失败,就给一个最安全的只读权限。
调用关系:read_thread 和 stored_thread_from_sqlite_metadata 在从数据库或元数据读线程时会用它。它处在“旧数据兼容”和“安全兜底”的位置,避免坏配置让线程获得过高权限。
调用图:被 2 处调用(read_thread, stored_thread_from_sqlite_metadata)。
permission_profile_to_metadata_value165–175 ↗
fn permission_profile_to_metadata_value(
permission_profile: &PermissionProfile,
) -> String
作用:把 PermissionProfile 转成可以存进元数据的字符串。这样权限设置才能跟线程一起保存下来,下次读取时再恢复。
数据流:输入是权限配置对象。它尝试把对象序列化成 JSON 字符串;成功就返回这段字符串;失败时会写一条警告日志,并返回空字符串。
调用关系:apply_metadata_update 更新线程元数据时会调用它。它和 permission_profile_from_metadata_value 是一进一出:一个负责保存,一个负责读取恢复。
调用图:被 1 处调用(apply_metadata_update);外部调用 3 个(new, to_string, warn!)。
distinct_thread_metadata_title177–184 ↗
fn distinct_thread_metadata_title(metadata: &ThreadMetadata) -> Option<String>
作用:从线程元数据里挑出真正值得显示的标题。如果标题为空,或者只是重复了用户第一句话,就不把它当成独立标题。
数据流:输入是 ThreadMetadata。它会去掉标题前后的空白,再和 first_user_message 做比较;如果标题有内容且不等于第一条用户消息,就返回这个标题,否则返回 None。
调用关系:线程列表、SQLite 元数据转线程、搜索结果命名时会用它。它帮上层界面避免显示重复、没意义的名字。
调用图:被 3 处调用(list_threads, stored_thread_from_sqlite_metadata, set_thread_search_result_names)。
set_thread_name_from_title186–191 ↗
fn set_thread_name_from_title(thread: &mut StoredThread, title: String)
作用:把一个合适的标题写到线程的 name 字段里。它会避开空标题,也避开和预览文字完全一样的标题。
数据流:输入是一个可修改的 StoredThread 和一个标题字符串。它先检查标题是否为空、是否和线程预览相同;如果都不是,就把 thread.name 设置成这个标题;否则不改动线程。
调用关系:线程列表、从日志路径读取线程、设置搜索结果名字时会调用它。通常 distinct_thread_metadata_title 先筛出标题,它再把标题真正放进线程对象。
调用图:被 3 处调用(list_threads, read_thread_from_rollout_path, set_thread_search_result_names)。
parse_rfc3339193–197 ↗
fn parse_rfc3339(value: Option<&str>) -> Option<DateTime<Utc>>
作用:把 RFC3339 格式的时间文字转成统一的 UTC 时间。RFC3339 是一种常见时间写法,比如带日期、时分秒和时区的字符串。
数据流:输入是一个可能为空的字符串。没有字符串就直接返回 None;有字符串就尝试解析,成功后转成 UTC 时间返回,失败也返回 None。
调用关系:stored_thread_from_rollout_item 用它处理日志里的创建时间和更新时间。它让上层不用关心时间字符串是否合法,只要拿到“有时间”或“没有时间”的结果。
调用图:被 1 处调用(stored_thread_from_rollout_item);外部调用 1 个(parse_from_rfc3339)。
parse_legacy_sandbox_policy199–211 ↗
fn parse_legacy_sandbox_policy(value: &str) -> serde_json::Result<SandboxPolicy>
作用:读取旧版本保存的沙箱策略。沙箱策略就是限制程序能访问什么资源的一套规则,比如只读、允许写工作区、完全开放等。
数据流:输入是一段旧权限字符串。它先尝试按 JSON 读取,再尝试把整段文字当成一个 JSON 字符串读取;如果还不行,就识别几个老名字,比如 read-only、workspace-write,并转成对应策略;无法识别时返回解析错误。
调用关系:permission_profile_from_metadata_value 在新版权限读取失败后会走到它。它的作用是让老数据在新系统里还能被看懂,而不是直接坏掉。
调用图:外部调用 1 个(from_str)。
git_info_from_parts213–226 ↗
fn git_info_from_parts(
sha: Option<String>,
branch: Option<String>,
origin_url: Option<String>,
) -> Option<GitInfo>
作用:把零散的 Git 信息拼成一个 GitInfo。Git 是代码版本管理工具,这里的信息包括提交号、分支名和远程仓库地址。
数据流:输入是可选的提交哈希、分支名和仓库地址。如果三个都没有,就返回 None;只要有任意一个,就生成 GitInfo,并把提交哈希包装成专门的 GitSha 类型。
调用关系:从日志、SQLite 元数据、按日志路径读取线程、更新元数据等流程都会用它。它把多个来源的 Git 字段整理成同一种形状,方便 StoredThread 使用。
调用图:被 5 处调用(stored_thread_from_rollout_item, read_thread_by_rollout_path, stored_thread_from_sqlite_metadata, apply_metadata_update, update_thread_metadata)。
thread_id_from_rollout_path228–240 ↗
fn thread_id_from_rollout_path(path: &Path) -> Option<ThreadId>
作用:尝试从会话日志文件名里抠出线程 ID。在线程条目本身没写 ID 时,它可以从文件名里补救。
数据流:输入是一个文件路径。它取文件名,先去掉可选的 .zst 压缩后缀,再要求结尾是 .jsonl;接着从文件名末尾取出 36 位 UUID 形状的字符串,并尝试转成 ThreadId;任何一步不符合都返回 None。
调用关系:stored_thread_from_rollout_item 在 ThreadItem 没有 thread_id 时会用它。它是旧日志或不完整日志的兜底识别办法。
调用图:调用 1 个内部函数(from_string);外部调用 1 个(file_name)。
tests::stored_thread_from_rollout_item_returns_logical_rollout_path251–272 ↗
fn stored_thread_from_rollout_item_returns_logical_rollout_path()
作用:这是一个测试,确认压缩日志文件会被转换成不带 .zst 的逻辑日志路径。这样系统内部看到的是统一的 .jsonl 路径,而不是受压缩格式影响的路径。
数据流:测试先造一个带 .jsonl.zst 后缀的假路径和固定 UUID,再调用 stored_thread_from_rollout_item。最后它比较结果里的 rollout_path,确认文件名已经变成 .jsonl,而不是仍保留 .zst。
调用关系:它专门验证 stored_thread_from_rollout_item 的一个重要行为。这个行为对读取压缩日志很关键,因为上层代码希望逻辑路径稳定,不随磁盘上是否压缩而变化。
调用图:调用 1 个内部函数(stored_thread_from_rollout_item);外部调用 5 个(default, from, from_u128, assert_eq!, format!)。
thread-store/src/local/mod.rs源码 ↗
这个文件是本地线程存储的总入口。线程可以理解成一次会话记录;本地保存时,完整历史会写进 rollout JSONL 文件(一行一条记录,方便重放),而 SQLite(一种本地小数据库)在可用时用来快速查标题、时间、归档状态等信息。文件里的 LocalThreadStore 像一个前台柜台:创建、追加、读取、搜索、归档、删除等请求都先到这里,再转交给旁边的小模块去做。它还保存一张“正在写入的线程”表,用互斥锁(一把锁,防止两个异步任务同时改同一份表)保护,避免同一个线程同时开两个写入器。一个重要规则是:直接追加历史只写文件,不自动更新 SQLite 元数据;想更新列表里看到的标题和摘要,要走 LiveThread 或显式更新元数据。
LocalThreadStoreConfig::from_config77–83 ↗
fn from_config(config: &impl codex_rollout::RolloutConfigView) -> Self
作用:从项目的通用配置里摘出本地线程存储需要的几项路径和默认模型提供方。这样创建本地仓库时不用到处手工拼配置。
数据流:输入是一份通用配置 → 它读取 Codex 主目录、SQLite 目录、默认模型提供方 ID → 输出一个 LocalThreadStoreConfig,后面本地读写文件和数据库都靠它找位置。
调用关系:很多会话创建和恢复流程会先调用它,把大配置缩成线程仓库专用配置;随后通常交给 LocalThreadStore::new 建出真正的本地仓库。
调用图:被 8 处调用(resume_agent_from_rollout_reads_archived_rollout_path, guardian_subagent_does_not_inherit_parent_exec_policy_rules, make_session_and_context, make_session_and_context_with_auth_config_home_and_rx, make_session_with_config_and_rx, make_session_with_history_source_and_agent_control_and_rx, session_new_fails_when_zsh_fork_enabled_without_packaged_zsh, thread_store_from_config);外部调用 3 个(codex_home, model_provider_id, sqlite_home)。
LocalThreadStore::fmt87–91 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
作用:给 LocalThreadStore 提供调试打印方式。它只打印配置,不把内部状态全部展开,避免日志太吵或泄露不必要细节。
数据流:输入是格式化器和当前仓库 → 它把结构名和 config 写进去 → 输出给 Rust 调试日志使用的文本表示。
调用关系:当代码或测试用 Debug 打印 LocalThreadStore 时会触发它;它只借助标准调试构造器,不参与实际存储流程。
调用图:外部调用 1 个(debug_struct)。
LocalThreadStore::new96–102 ↗
fn new(config: LocalThreadStoreConfig, state_db: Option<StateDbHandle>) -> Self
作用:创建一个本地线程仓库实例。它把配置、正在写入的线程表、可选 SQLite 句柄放到一起。
数据流:输入是本地存储配置和一个可选的状态数据库句柄 → 它新建一张空的 live_recorders 表,并用共享指针和异步锁包起来 → 输出可被克隆、可并发使用的 LocalThreadStore。
调用关系:配置准备好后,各种会话启动、测试和线程存储工厂都会调用它;之后所有创建、读取、追加、搜索等操作都会从这个实例出发。
调用图:被 75 处调用(resume_agent_from_rollout_reads_archived_rollout_path, has_recorded_sessions, guardian_subagent_does_not_inherit_parent_exec_policy_rules, make_session_and_context, make_session_and_context_with_auth_config_home_and_rx, make_session_with_config_and_rx, make_session_with_history_source_and_agent_control_and_rx, session_new_fails_when_zsh_fork_enabled_without_packaged_zsh, with_models_provider_home_and_state_for_tests, thread_store_from_config (+15 more));外部调用 3 个(new, new, new)。
LocalThreadStore::state_db105–107 ↗
async fn state_db(&self) -> Option<StateDbHandle>
作用:取出这个仓库正在使用的 SQLite 状态数据库句柄。需要查或改元数据的模块会用它。
数据流:输入是当前仓库 → 它克隆保存着的可选数据库句柄 → 输出 Some 数据库句柄,或在没有 SQLite 时输出 None。
调用关系:归档、删除、列表、搜索、解析 rollout 路径、同步已落盘路径等流程会调用它;如果没有数据库,相关模块就会走文件兼容路径或跳过数据库更新。
调用图:被 14 处调用(archive_thread, delete_thread, list_threads, sync_materialized_rollout_path, read_sqlite_metadata, resolve_rollout_path, search_threads, set_thread_search_result_names, unarchive_thread, apply_metadata_update (+4 more))。
LocalThreadStore::live_rollout_path126–128 ↗
async fn live_rollout_path(&self, thread_id: ThreadId) -> ThreadStoreResult<PathBuf>
作用:查某个正在写入的线程当前对应的 rollout 文件路径。旧的只认本地文件路径的代码会用它。
数据流:输入是线程 ID → 它转交给 live_writer 查当前写入器的文件位置 → 输出该线程正在写的 JSONL 文件路径,找不到则返回错误。
调用关系:它是给外部或测试读取“活跃线程文件在哪里”的小入口;实际查找工作交给 live_writer::rollout_path。
调用图:调用 1 个内部函数(rollout_path)。
LocalThreadStore::live_recorder130–140 ↗
async fn live_recorder(
&self,
thread_id: ThreadId,
) -> ThreadStoreResult<RolloutRecorder>
作用:取出某个线程的实时写入器。没有这个写入器,就不能继续给该线程追加、刷新或关闭。
数据流:输入是线程 ID → 它锁住 live_recorders 表并查找对应 RolloutRecorder → 输出写入器副本;如果表里没有,就返回“线程不存在”。
调用关系:append_items、flush_thread、persist_thread、shutdown_thread 等实时写入操作会用它拿到真正干活的记录器。
调用图:被 4 处调用(append_items, flush_thread, persist_thread, shutdown_thread)。
LocalThreadStore::ensure_live_recorder_absent142–152 ↗
async fn ensure_live_recorder_absent(
&self,
thread_id: ThreadId,
) -> ThreadStoreResult<()>
作用:确认某个线程现在还没有活跃写入器。它用来防止同一个线程被重复创建或重复恢复。
数据流:输入是线程 ID → 它锁住 live_recorders 表检查是否已存在 → 不存在就返回成功;存在就返回“请求无效”,提示已有 live writer。
调用关系:create_thread 和 resume_thread 在打开新写入器前会先调用它;这是防止双写同一个文件的重要关卡。
调用图:被 2 处调用(create_thread, resume_thread);外部调用 1 个(format!)。
LocalThreadStore::insert_live_recorder154–168 ↗
async fn insert_live_recorder(
&self,
thread_id: ThreadId,
recorder: RolloutRecorder,
) -> ThreadStoreResult<()>
作用:把新打开的实时写入器登记到仓库里。登记后,后续追加和刷新才能找到它。
数据流:输入是线程 ID 和 RolloutRecorder → 它锁住 live_recorders 表并尝试插入 → 如果位置空着就保存并成功;如果已经有人占着就报重复写入器错误。
调用关系:create_thread 和 resume_thread 在成功打开文件写入器后调用它;它和 ensure_live_recorder_absent 一前一后,保证同一线程只有一个活跃写入器。
调用图:被 2 处调用(create_thread, resume_thread);外部调用 1 个(format!)。
LocalThreadStore::read_thread_by_rollout_path_params213–224 ↗
async fn read_thread_by_rollout_path_params(
&self,
params: ReadThreadByRolloutPathParams,
) -> ThreadStoreResult<StoredThread>
作用:把“按路径读取线程”的参数对象拆开,然后交给读取模块。它是 trait 接口和具体读取函数之间的适配层。
数据流:输入是包含 rollout 路径、是否包含归档、是否带历史的参数 → 它把这些字段传给 read_thread 模块 → 输出读取到的 StoredThread 或错误。
调用关系:ThreadStore trait 的 read_thread_by_rollout_path 会调用它;真正解析文件和组装线程信息的工作在 read_thread::read_thread_by_rollout_path。
调用图:调用 1 个内部函数(read_thread_by_rollout_path);被 1 处调用(read_thread_by_rollout_path)。
LocalThreadStore::as_any228–230 ↗
fn as_any(&self) -> &dyn std::any::Any
作用:把 LocalThreadStore 暴露成通用 Any 类型,方便少数地方做类型识别或向下转换。普通业务读写不会直接关心它。
数据流:输入是当前仓库引用 → 它原样返回一个 dyn Any 视角的引用 → 不改动任何数据。
调用关系:这是 ThreadStore trait 的通用钩子;当上层拿到的是抽象 ThreadStore,但需要确认底层是不是 LocalThreadStore 时会用到。
LocalThreadStore::create_thread232–234 ↗
fn create_thread(&self, params: CreateThreadParams) -> ThreadStoreFuture<'_, ()>
作用:开始一个新的本地线程写入流程。它不是自己写文件,而是把活交给 live_writer 模块。
数据流:输入是创建线程所需参数,比如线程 ID、来源、基础指令和持久化元数据 → 它包装成异步任务并调用 live_writer::create_thread → 输出成功或具体错误。
调用关系:上层通过 ThreadStore trait 创建线程时会进到这里;后续写入器登记、文件创建等细节由 live_writer 继续处理。
调用图:调用 1 个内部函数(create_thread);外部调用 1 个(pin)。
LocalThreadStore::resume_thread236–238 ↗
fn resume_thread(&self, params: ResumeThreadParams) -> ThreadStoreFuture<'_, ()>
作用:重新打开一个已有线程,让它可以继续追加新记录。常见于恢复旧会话或接着外部 rollout 文件写。
数据流:输入是恢复参数,包括线程 ID、可选路径、可选历史和元数据 → 它启动异步任务并交给 live_writer::resume_thread → 输出恢复成功或错误。
调用关系:会话恢复流程调用它;live_writer 会负责找文件、加载旧历史、打开新写入器并登记到仓库。
调用图:调用 1 个内部函数(resume_thread);外部调用 1 个(pin)。
LocalThreadStore::append_items240–242 ↗
fn append_items(&self, params: AppendThreadItemsParams) -> ThreadStoreFuture<'_, ()>
作用:往一个正在运行的线程里追加历史条目。这里追加的是原始历史,不负责自动改标题或摘要。
数据流:输入是线程 ID 和一批 rollout 条目 → 它把请求交给 live_writer::append_items → 结果是这些条目被写入对应实时写入器,或因为线程没打开而失败。
调用关系:LiveThread 或其他调用者写新消息时会走这里;实际写文件前 live_writer 会通过 live_recorder 找到对应 RolloutRecorder。
调用图:调用 1 个内部函数(append_items);外部调用 1 个(pin)。
LocalThreadStore::persist_thread244–246 ↗
fn persist_thread(&self, thread_id: ThreadId) -> ThreadStoreFuture<'_, ()>
作用:要求某个活跃线程真正落到磁盘上。它用于把还在内存或临时状态里的内容变成持久文件。
数据流:输入是线程 ID → 它转给 live_writer::persist_thread → 输出成功或错误,并可能让 rollout 文件从临时状态变成可长期保存的状态。
调用关系:会话需要确保记录不会丢时会调用它;具体文件落盘动作由 live_writer 和 RolloutRecorder 完成。
调用图:调用 1 个内部函数(persist_thread);外部调用 1 个(pin)。
LocalThreadStore::flush_thread248–250 ↗
fn flush_thread(&self, thread_id: ThreadId) -> ThreadStoreFuture<'_, ()>
作用:把某个线程已经写入但可能还缓存在内存里的内容冲到磁盘。就像写完文档后点一下保存。
数据流:输入是线程 ID → 它调用 live_writer::flush_thread → 输出刷新成功或错误,成功后文件内容更可靠地出现在磁盘上。
调用关系:测试、关闭前、需要立即可读时会调用它;live_writer 会找到 live_recorder 并让底层记录器 flush。
调用图:调用 1 个内部函数(flush_thread);外部调用 1 个(pin)。
LocalThreadStore::shutdown_thread252–254 ↗
fn shutdown_thread(&self, thread_id: ThreadId) -> ThreadStoreFuture<'_, ()>
作用:关闭某个活跃线程的写入器。关闭后再追加就会失败,避免继续写到已经结束的会话。
数据流:输入是线程 ID → 它交给 live_writer::shutdown_thread → 输出成功或错误,并通常会从 live_recorders 表里移除该线程的写入器。
调用关系:会话结束时调用;后续 append_items 如果再找这个线程,就会通过 live_recorder 得到 ThreadNotFound。
调用图:调用 1 个内部函数(shutdown_thread);外部调用 1 个(pin)。
LocalThreadStore::discard_thread256–258 ↗
fn discard_thread(&self, thread_id: ThreadId) -> ThreadStoreFuture<'_, ()>
作用:丢弃一个活跃线程,尤其是还没真正产生内容的线程。它用于避免留下空文件或无用记录。
数据流:输入是线程 ID → 它调用 live_writer::discard_thread → 输出成功或错误,并会清理活跃写入器及可能未物化的文件。
调用关系:创建后又取消、没有必要保存时会用它;它和 shutdown_thread 类似都会让后续追加找不到 live writer,但语义是“不要保存”。
调用图:调用 1 个内部函数(discard_thread);外部调用 1 个(pin)。
LocalThreadStore::load_history260–265 ↗
fn load_history(
&self,
params: LoadThreadHistoryParams,
) -> ThreadStoreFuture<'_, StoredThreadHistory>
作用:读取某个线程的完整历史。它会优先考虑正在写入的 live 文件,这样恢复中的外部路径也能读对。
数据流:输入是线程 ID 和是否允许读取归档线程 → 它先尝试找到 live writer 的 rollout 路径;如果找到了,会检查归档限制,再按路径读取完整历史;如果没有 live writer,就按线程 ID 普通读取 → 输出 StoredThreadHistory,或在历史缺失、归档不允许时返回错误。
调用关系:ThreadStore trait 的 load_history 会进入这里;它把路径判断、归档检查和 read_thread 模块串起来,是读取历史时的重要分流点。
调用图:调用 4 个内部函数(rollout_path_is_archived, rollout_path, read_thread, read_thread_by_rollout_path);外部调用 2 个(pin, format!)。
LocalThreadStore::read_thread267–269 ↗
fn read_thread(&self, params: ReadThreadParams) -> ThreadStoreFuture<'_, StoredThread>
作用:按线程 ID 读取一个线程的基本信息,并可选择带上完整历史。它是最常用的单线程读取入口。
数据流:输入是读取参数,包括线程 ID、是否包含归档、是否包含历史 → 它转交给 read_thread::read_thread → 输出 StoredThread 或错误。
调用关系:上层想展示或恢复某个线程时调用它;具体从 SQLite 查元数据、从 JSONL 补历史等工作由 read_thread 模块完成。
调用图:调用 1 个内部函数(read_thread);外部调用 1 个(pin)。
LocalThreadStore::read_thread_by_rollout_path271–278 ↗
fn read_thread_by_rollout_path(
&self,
params: ReadThreadByRolloutPathParams,
) -> ThreadStoreFuture<'_, StoredThread>
作用:按 rollout 文件路径读取线程,而不是按线程 ID。适合处理外部文件、旧文件或已知具体路径的场景。
数据流:输入是路径读取参数 → 它包装成异步任务,并经 read_thread_by_rollout_path_params 交给读取模块 → 输出对应 StoredThread。
调用关系:这是 ThreadStore trait 暴露的按路径读取入口;内部适配后由 read_thread::read_thread_by_rollout_path 做实际解析。
调用图:调用 2 个内部函数(read_thread_by_rollout_path_params, read_thread_by_rollout_path);外部调用 1 个(pin)。
LocalThreadStore::list_threads280–282 ↗
fn list_threads(&self, params: ListThreadsParams) -> ThreadStoreFuture<'_, ThreadPage>
作用:分页列出本地保存的线程。它让界面可以显示“最近会话列表”。
数据流:输入是分页、过滤等列表参数 → 它调用 list_threads::list_threads → 输出一页 ThreadPage,里面是线程摘要和翻页信息。
调用关系:例如 has_threads 这类检查或会话列表页面会用它;实际查询 SQLite 或扫描兼容文件的工作在 list_threads 模块。
调用图:调用 1 个内部函数(list_threads);被 1 处调用(has_threads);外部调用 1 个(pin)。
LocalThreadStore::search_threads284–289 ↗
fn search_threads(
&self,
params: SearchThreadsParams,
) -> ThreadStoreFuture<'_, ThreadSearchPage>
作用:在本地线程里搜索关键字。它用于让用户从旧会话中找内容。
数据流:输入是搜索参数 → 它转给 search_threads::search_threads → 输出 ThreadSearchPage,包含匹配结果和分页信息。
调用关系:搜索入口调用这个 trait 方法;search_threads 模块会负责查索引、读必要文件并组织搜索结果。
调用图:调用 1 个内部函数(search_threads);外部调用 1 个(pin)。
LocalThreadStore::update_thread_metadata291–296 ↗
fn update_thread_metadata(
&self,
params: UpdateThreadMetadataParams,
) -> ThreadStoreFuture<'_, StoredThread>
作用:更新线程的元数据,比如标题、摘要、第一条用户消息等。它弥补了“原始追加只写历史、不改索引”的规则。
数据流:输入是元数据更新参数 → 它调用 update_thread_metadata::update_thread_metadata → 输出更新后的 StoredThread,或在找不到/写失败时返回错误。
调用关系:LiveThread 观察到新消息后通常会用它更新 SQLite;真正的数据库写入和兼容处理在 update_thread_metadata 模块。
调用图:调用 1 个内部函数(update_thread_metadata);外部调用 1 个(pin)。
LocalThreadStore::archive_thread298–300 ↗
fn archive_thread(&self, params: ArchiveThreadParams) -> ThreadStoreFuture<'_, ()>
作用:把线程标记为归档。归档后,普通只看活跃线程的读取和列表通常会把它排除。
数据流:输入是归档参数 → 它交给 archive_thread::archive_thread → 输出成功或错误,并改变该线程的归档状态。
调用关系:用户归档会话时调用;归档模块会处理 SQLite 状态和本地文件兼容行为。
调用图:调用 1 个内部函数(archive_thread);外部调用 1 个(pin)。
LocalThreadStore::unarchive_thread302–304 ↗
fn unarchive_thread(&self, params: ArchiveThreadParams) -> ThreadStoreFuture<'_, StoredThread>
作用:把已归档线程恢复为普通线程,并返回恢复后的线程信息。用于用户从归档箱拿回会话。
数据流:输入是包含线程 ID 的参数 → 它调用 unarchive_thread::unarchive_thread → 输出恢复后的 StoredThread 或错误。
调用关系:取消归档操作会走这里;具体状态修改和读取结果组装由 unarchive_thread 模块完成。
调用图:调用 1 个内部函数(unarchive_thread);外部调用 1 个(pin)。
LocalThreadStore::delete_thread306–308 ↗
fn delete_thread(&self, params: DeleteThreadParams) -> ThreadStoreFuture<'_, ()>
作用:删除一个本地线程。它用于真正清理不再需要的会话记录。
数据流:输入是删除参数 → 它调用 delete_thread::delete_thread → 输出成功或错误,并可能移除数据库记录和本地文件。
调用关系:用户删除会话或清理数据时调用;delete_thread 模块负责实际删数据库和文件。
调用图:调用 1 个内部函数(delete_thread);外部调用 1 个(pin)。
tests::live_writer_lifecycle_writes_and_closes332–378 ↗
async fn live_writer_lifecycle_writes_and_closes()
作用:测试实时写入器从创建、追加、保存、刷新到关闭的完整生命周期是否正常。重点是关闭后不能再写。
数据流:测试先建临时目录和仓库 → 创建线程、追加一条用户消息、保存并刷新,再检查文件里确实有消息 → 关闭线程后再追加,期望得到 ThreadNotFound 错误。
调用关系:它调用 create_thread_params 和 assert_rollout_contains_message 等测试工具,覆盖 LocalThreadStore 的 create、append、persist、flush、shutdown 组合行为。
调用图:调用 3 个内部函数(default, new, test_config);外部调用 5 个(new, assert!, assert_rollout_contains_message, create_thread_params, vec!)。
tests::raw_append_items_does_not_update_sqlite_metadata381–415 ↗
async fn raw_append_items_does_not_update_sqlite_metadata()
作用:测试一个关键约定:直接 append_items 只写历史文件,不自动更新 SQLite 元数据。这样可以避免调用者误以为原始写入会更新列表标题。
数据流:测试建立带 SQLite 的仓库 → 创建线程并直接追加一条消息,再刷新 → 去 SQLite 查询该线程元数据,期望仍然没有记录。
调用关系:它固定 ThreadStore 的契约;和后面的 LiveThread 测试形成对照,说明元数据更新必须由更高层观察后显式触发。
调用图:调用 4 个内部函数(default, init, new, test_config);外部调用 4 个(new, assert_eq!, create_thread_params, vec!)。
tests::live_thread_observes_appended_items_into_sqlite_metadata418–450 ↗
async fn live_thread_observes_appended_items_into_sqlite_metadata()
作用:测试 LiveThread 会观察追加的用户消息,并把它转成 SQLite 元数据。也就是列表里能看到标题、摘要和第一条消息。
数据流:测试创建带 SQLite 的仓库和 LiveThread → 追加一条用户消息并刷新 → 从 SQLite 读元数据,检查 first_user_message、preview、title 都变成这条消息。
调用关系:它通过 LiveThread::create 走高层封装,而不是直接 append_items;用来证明元数据更新发生在 LiveThread 这一层。
调用图:调用 5 个内部函数(default, init, create, new, test_config);外部调用 5 个(new, new, assert_eq!, create_thread_params, user_message_item)。
tests::live_thread_shutdown_does_not_materialize_empty_thread_metadata453–486 ↗
async fn live_thread_shutdown_does_not_materialize_empty_thread_metadata()
作用:测试空线程关闭时不会留下空文件和空元数据。这样用户取消一个没内容的会话时,本地不会多出垃圾记录。
数据流:测试创建 LiveThread 但不追加内容 → 取得可能的 rollout 路径后直接 shutdown → 检查文件不存在,SQLite 里也没有该线程记录。
调用关系:它覆盖 LiveThread 关闭路径和 LocalThreadStore 的本地写入行为,确保“空会话不物化”这个体验规则成立。
调用图:调用 5 个内部函数(default, init, create, new, test_config);外部调用 5 个(new, new, assert!, assert_eq!, create_thread_params)。
tests::live_thread_shutdown_with_buffered_items_materializes_before_metadata_read489–530 ↗
async fn live_thread_shutdown_with_buffered_items_materializes_before_metadata_read()
作用:测试即使只追加了看似没有文字内容的条目,关闭时也会先把线程物化出来。这样元数据能记录正确的 rollout 路径。
数据流:测试创建 LiveThread → 追加一个 token count 事件 → shutdown → 检查 rollout 文件存在,并且 SQLite 元数据里的路径等于该文件路径。
调用关系:它验证 LiveThread 关闭时会先处理缓冲内容,再让元数据可读;避免数据库指向一个没有落盘的线程。
调用图:调用 5 个内部函数(default, init, create, new, test_config);外部调用 7 个(new, new, assert!, assert_eq!, TokenCount, EventMsg, create_thread_params)。
tests::live_thread_resume_loads_history_before_observing_metadata533–583 ↗
async fn live_thread_resume_loads_history_before_observing_metadata()
作用:测试恢复旧本地线程时,会先读旧历史,再根据旧历史确定元数据。这样新追加不会覆盖原始创建时间、模型提供方和首条消息。
数据流:测试写入一个旧 session 文件 → 用 LiveThread::resume 恢复它并追加新消息 → 查询 SQLite,期望创建时间、模型提供方、首条用户消息仍来自旧文件。
调用关系:它覆盖恢复流程中 read history 和 observe metadata 的顺序;依赖 write_session_file 准备旧 rollout 文件。
调用图:调用 6 个内部函数(from_string, init, resume, new, test_config, write_session_file);外部调用 5 个(new, new, assert_eq!, user_message_item, from_u128)。
tests::live_thread_resume_loads_history_from_explicit_external_rollout_path586–637 ↗
async fn live_thread_resume_loads_history_from_explicit_external_rollout_path()
作用:测试恢复时如果给了一个外部目录里的 rollout 路径,也会从那个路径读旧历史。这样用户或工具传入外部文件时不会读错本地默认位置。
数据流:测试在另一个临时目录写 session 文件 → 用显式路径恢复 LiveThread 并追加 → 查询 SQLite,确认元数据来自外部文件里的旧历史。
调用关系:它和上一个恢复测试类似,但重点是外部 rollout_path;证明 resume 参数里的路径优先有效。
调用图:调用 6 个内部函数(from_string, init, resume, new, test_config, write_session_file);外部调用 5 个(new, new, assert_eq!, user_message_item, from_u128)。
tests::create_thread_rejects_missing_cwd640–657 ↗
async fn create_thread_rejects_missing_cwd()
作用:测试创建本地线程时必须提供 cwd,也就是当前工作目录。没有 cwd,后面很难知道这次会话属于哪个项目位置。
数据流:测试构造创建参数后把 metadata.cwd 改成 None → 调用 create_thread → 期望得到 InvalidRequest,并且错误文字说明本地仓库需要 cwd。
调用关系:它覆盖 live_writer::create_thread 前置校验;create_thread_params 先生成正常参数,再由测试故意破坏。
调用图:调用 3 个内部函数(default, new, test_config);外部调用 3 个(new, assert!, create_thread_params)。
tests::discard_thread_drops_unmaterialized_live_writer660–693 ↗
async fn discard_thread_drops_unmaterialized_live_writer()
作用:测试丢弃一个还没真正落盘的线程会清掉写入器和文件。丢弃后再写应该失败。
数据流:测试创建线程并取得 rollout 路径 → 调用 discard_thread → 检查文件不存在;然后尝试 append_items,期望 ThreadNotFound。
调用关系:它覆盖 discard_thread 与 live_recorders 表的关系,确认丢弃不仅删文件,也从活跃写入器表里移除。
调用图:调用 3 个内部函数(default, new, test_config);外部调用 4 个(new, assert!, create_thread_params, vec!)。
tests::resume_thread_reopens_live_writer_and_appends696–755 ↗
async fn resume_thread_reopens_live_writer_and_appends()
作用:测试一个线程关闭后,可以用新的仓库实例恢复写入并继续追加。这样程序重启后也能接着写旧会话。
数据流:测试先创建并写入“before resume”,保存刷新后关闭 → 用另一个 LocalThreadStore 恢复同一线程 → 追加“after resume”并刷新 → 检查同一个 rollout 文件包含两条消息。
调用关系:它把 create、append、persist、shutdown、resume 串起来,证明 live_writer 可以重新打开旧文件继续写。
调用图:调用 3 个内部函数(default, new, test_config);外部调用 5 个(new, assert_rollout_contains_message, create_thread_params, thread_metadata, vec!)。
tests::create_thread_rejects_duplicate_live_writer758–775 ↗
async fn create_thread_rejects_duplicate_live_writer()
作用:测试同一个线程不能被创建两次活跃写入器。否则两个写入器可能同时写一个文件,造成内容乱序或损坏。
数据流:测试先成功 create_thread → 再用同一个线程 ID 创建一次 → 期望 InvalidRequest,错误里包含 already has a live local writer。
调用关系:它直接验证 ensure_live_recorder_absent 和 insert_live_recorder 的防重复保护。
调用图:调用 3 个内部函数(default, new, test_config);外部调用 3 个(new, assert!, create_thread_params)。
tests::resume_thread_rejects_duplicate_live_writer778–803 ↗
async fn resume_thread_rejects_duplicate_live_writer()
作用:测试已有活跃写入器时不能再恢复同一个线程。恢复和创建一样,都必须避免双写。
数据流:测试先创建线程并取得 live rollout 路径 → 再对同一线程调用 resume_thread → 期望 InvalidRequest,并提示已有 live local writer。
调用关系:它覆盖 resume_thread 的防重复路径,和 create_thread 的重复创建测试互相补充。
调用图:调用 3 个内部函数(default, new, test_config);外部调用 4 个(new, assert!, create_thread_params, thread_metadata)。
tests::resume_thread_rejects_missing_cwd806–830 ↗
async fn resume_thread_rejects_missing_cwd()
作用:测试恢复本地线程时也必须提供 cwd。恢复不是只打开文件,还要保留这次会话的项目上下文。
数据流:测试先写一个旧 session 文件 → 构造 ResumeThreadParams,但把 metadata.cwd 设为 None → 调用 resume_thread,期望请求无效并提示需要 cwd。
调用关系:它覆盖 live_writer::resume_thread 的参数校验;write_session_file 只负责准备可恢复的旧历史。
调用图:调用 4 个内部函数(from_string, new, test_config, write_session_file);外部调用 3 个(new, assert!, from_u128)。
tests::load_history_uses_live_writer_rollout_path833–878 ↗
async fn load_history_uses_live_writer_rollout_path()
作用:测试读取历史时会优先使用 live writer 当前的 rollout 路径,尤其是恢复了外部文件的情况。否则可能按线程 ID 找到错误位置。
数据流:测试在外部目录写旧 session 文件 → 用该路径恢复线程并追加新消息 → 调用 load_history → 期望历史里包含新追加的外部历史项。
调用关系:它验证 LocalThreadStore::load_history 的第一步路径分流:如果有 live writer,就按 live writer 的路径读。
调用图:调用 4 个内部函数(from_string, new, test_config, write_session_file);外部调用 5 个(new, assert!, thread_metadata, from_u128, vec!)。
tests::read_thread_uses_live_writer_rollout_path_for_external_resume881–917 ↗
async fn read_thread_uses_live_writer_rollout_path_for_external_resume()
作用:测试按线程 ID 读取一个从外部路径恢复的活跃线程时,也能返回外部 rollout 路径和正确历史。
数据流:测试创建外部 session 文件并恢复 → 调用 read_thread,要求带历史 → 检查返回的 rollout_path 等于外部路径,历史里有旧文件的用户消息。
调用关系:它覆盖 read_thread 模块与 live writer 路径之间的配合,避免活跃外部线程被误读成本地默认文件。
调用图:调用 4 个内部函数(from_string, new, test_config, write_session_file);外部调用 5 个(new, assert!, assert_eq!, thread_metadata, from_u128)。
tests::load_history_uses_live_writer_rollout_path_for_archived_source920–984 ↗
async fn load_history_uses_live_writer_rollout_path_for_archived_source()
作用:测试活跃线程如果来自归档文件,读取时仍会尊重 include_archived 开关。也就是说路径来自 live writer,但归档规则不能绕过。
数据流:测试准备一个归档 session 文件并恢复为 live writer → 追加并刷新 → 不允许归档时 read_thread 和 load_history 都应报错;允许归档时 load_history 能读到追加消息。
调用关系:它专门覆盖 LocalThreadStore::load_history 里 rollout_path_is_archived 的检查,以及 read_thread 对归档 live 路径的处理。
调用图:调用 4 个内部函数(from_string, new, test_config, write_archived_session_file);外部调用 5 个(new, assert!, thread_metadata, from_u128, vec!)。
tests::read_thread_by_rollout_path_includes_history987–1029 ↗
async fn read_thread_by_rollout_path_includes_history()
作用:测试按 rollout 路径读取线程时,如果要求包含历史,返回结果里真的有历史条目。
数据流:测试创建线程、追加一条用户消息并刷新 → 取得 live rollout 路径 → 按路径读取并要求 include_history → 检查线程 ID 正确,且历史里有一条用户消息。
调用关系:它覆盖 LocalThreadStore 的按路径读取入口,证明 read_thread_by_rollout_path 参数能正确传到读取模块。
调用图:调用 3 个内部函数(default, new, test_config);外部调用 4 个(new, assert_eq!, create_thread_params, vec!)。
tests::create_thread_params1031–1044 ↗
fn create_thread_params(thread_id: ThreadId) -> CreateThreadParams
作用:生成一份测试用的标准创建线程参数。这样每个测试不用重复写一大堆默认字段。
数据流:输入是线程 ID → 它填入默认来源、基础指令、空动态工具和 thread_metadata → 输出 CreateThreadParams。
调用关系:多个创建相关测试都会调用它;测试如果要验证异常情况,会先拿它生成正常参数,再修改其中某个字段。
调用图:调用 1 个内部函数(default);外部调用 2 个(new, thread_metadata)。
tests::thread_metadata1046–1052 ↗
fn thread_metadata() -> ThreadPersistenceMetadata
作用:生成测试用的线程持久化元数据,包括 cwd、模型提供方和记忆模式。它代表一个最小但合法的本地线程上下文。
数据流:没有显式输入 → 它读取当前工作目录,填入 test-provider 和 Enabled 记忆模式 → 输出 ThreadPersistenceMetadata。
调用关系:create_thread_params 和恢复相关测试都会用它,保证测试默认满足本地仓库需要 cwd 的规则。
调用图:外部调用 1 个(current_dir)。
tests::user_message_item1054–1063 ↗
tests::assert_rollout_contains_message1065–1075 ↗
async fn assert_rollout_contains_message(path: &std::path::Path, expected: &str)
作用:检查某个 rollout 文件里是否包含指定用户消息。它是测试里的小断言工具。
数据流:输入是文件路径和期望文字 → 它用 RolloutRecorder::load_rollout_items 读出所有条目 → 如果找到匹配的用户消息就通过,否则断言失败。
调用关系:生命周期和恢复写入测试会调用它,确认追加操作确实写进了磁盘上的 JSONL 历史文件。
调用图:调用 1 个内部函数(load_rollout_items);外部调用 1 个(assert!)。
本地线程生命周期操作
这些文件涵盖本地线程存储的主要行为,包括实时写入、重建已存线程、浏览和搜索线程,以及删除持久化数据。
thread-store/src/local/live_writer.rs源码 ↗
可以把一个线程理解成一场聊天或一次任务的完整流水账。这个文件管的是“活着的流水账”:创建时打开一个记录器,恢复时接上旧记录,追加新内容时写进本地 rollout 文件(一种按顺序保存事件的记录文件),保存、刷新、关闭时再确保文件和数据库里的路径信息对齐。它最重要的地方是顺序很谨慎:追加内容后会等待写盘完成,再让外层更新元数据。这样就不会出现数据库说“这条消息已经有了”,但 JSONL 记录文件里其实还没落盘的尴尬情况。它还会在关闭线程时把记录器从内存里移走;如果只是丢弃线程,则只从内存表里删掉,不做额外写盘。最后,所有底层磁盘错误都会被包装成线程存储系统能理解的错误。
create_thread20–28 ↗
async fn create_thread(
store: &LocalThreadStore,
params: CreateThreadParams,
) -> ThreadStoreResult<()>
作用:创建一个新的本地线程记录器,让后续消息有地方写。它会先确认同一个线程还没有正在运行的记录器,避免两个记录器同时写同一份流水账。
数据流:输入是本地线程仓库和创建线程的参数,其中包含线程编号等信息。它先取出线程编号,检查内存里没有这个线程的 live recorder(正在写的记录器),再调用底层创建线程的流程生成记录器,最后把这个记录器放进仓库的内存表。输出是成功或错误;成功后,这个线程就进入“可继续写入”的状态。
调用关系:它处在新线程写入流程的开头。外层的 create_thread 会调用它;它自己先找 store 做防重检查,再把真正创建文件和记录器的工作交给 super::create_thread::create_thread,最后交回 store 保存这个活跃记录器。
调用图:调用 3 个内部函数(ensure_live_recorder_absent, insert_live_recorder, create_thread);被 1 处调用(create_thread)。
resume_thread30–75 ↗
async fn resume_thread(
store: &LocalThreadStore,
params: ResumeThreadParams,
) -> ThreadStoreResult<()>
作用:恢复一个已经存在的线程,让它可以继续往原来的本地记录文件后面写。它解决的是“程序重启或线程暂停后,怎么接着写同一份流水账”的问题。
数据流:输入是本地线程仓库和恢复参数。它先确认内存里没有同线程的活跃记录器;如果参数里已经给了 rollout 文件路径,就直接用;如果没给,就读取线程信息来找到旧路径。然后它检查元数据里必须有当前工作目录 cwd(也就是这次任务运行在哪个文件夹),用这些信息拼出 RolloutConfig,再用 RolloutRecorder::new 以“恢复”模式打开记录器。成功后,它把记录器插回内存表;失败则返回线程存储错误。
调用关系:它是恢复线程流程的入口之一,由外层 resume_thread 调用。它可能会请 read_thread 帮忙读取旧线程信息,也会用 RolloutRecorderParams::resume 告诉 RolloutRecorder“不要新建,从旧文件接着来”,最后仍然通过 store.insert_live_recorder 把恢复好的记录器登记起来。
调用图:调用 5 个内部函数(new, resume, ensure_live_recorder_absent, insert_live_recorder, read_thread);被 1 处调用(resume_thread);外部调用 1 个(matches!)。
append_items77–93 ↗
async fn append_items(
store: &LocalThreadStore,
params: AppendThreadItemsParams,
) -> ThreadStoreResult<()>
作用:把新的线程内容追加到正在写的本地记录文件里。它会先过滤成真正需要持久保存的内容,并且保证这些内容写完、刷到本地写入器后才返回。
数据流:输入是本地线程仓库和一批要追加的线程条目。它先用 persisted_rollout_items 挑出需要长期保存的规范化条目;如果没有可保存内容,就直接成功返回。否则它根据线程编号找到活跃记录器,把这些条目写进去,然后调用 flush 等待写入完成。输出是成功或错误;成功后,记录文件已经追上这次追加。
调用关系:它被外层 append_items 流程调用,是实时追加消息的关键一步。它向 store.live_recorder 要到对应记录器,再把具体写文件的事交给 recorder.record_canonical_items 和 recorder.flush。注释中特别说明:外层会在它返回后立刻更新元数据,所以这里必须等本地写完,防止 SQLite 数据库跑到 JSONL 文件前面。
调用图:调用 1 个内部函数(live_recorder);被 1 处调用(append_items);外部调用 1 个(persisted_rollout_items)。
persist_thread95–106 ↗
async fn persist_thread(
store: &LocalThreadStore,
thread_id: ThreadId,
) -> ThreadStoreResult<()>
作用:要求某个活跃线程把当前内容持久保存下来。简单说,就是告诉记录器“该正式落盘了”,然后同步数据库里保存的记录文件路径。
数据流:输入是本地线程仓库和线程编号。它先找到这个线程的活跃记录器,调用 persist 做持久化;如果底层出现输入输出错误,就转成线程存储错误。持久化后,它调用 sync_materialized_rollout_path 检查实际生成的 rollout 路径,并在需要时更新数据库。输出是成功或错误;成功后,文件和数据库路径尽量保持一致。
调用关系:它被外层 persist_thread 调用,也会在 update_thread_metadata 这种更新线程元数据的流程中用到。它不自己碰数据库细节,而是把路径同步交给 sync_materialized_rollout_path。
调用图:调用 2 个内部函数(live_recorder, sync_materialized_rollout_path);被 2 处调用(persist_thread, update_thread_metadata)。
flush_thread108–119 ↗
async fn flush_thread(
store: &LocalThreadStore,
thread_id: ThreadId,
) -> ThreadStoreResult<()>
作用:把某个线程已经排队的写入尽快刷出去。它像是按一下“保存进度”按钮,确保本地记录器把手头内容写完。
数据流:输入是本地线程仓库和线程编号。它找到活跃记录器,调用 flush 等待写入完成;如果写盘失败,就包装成统一错误。之后它同步 rollout 路径到数据库。输出是成功或错误;成功代表这次刷新已经完成,并且路径信息也被检查过。
调用关系:它由外层 flush_thread 调用,常用于需要确认数据已经写出的时刻。它把实际刷盘交给 recorder.flush,把数据库路径修正交给 sync_materialized_rollout_path。
调用图:调用 2 个内部函数(live_recorder, sync_materialized_rollout_path);被 1 处调用(flush_thread)。
shutdown_thread121–130 ↗
async fn shutdown_thread(
store: &LocalThreadStore,
thread_id: ThreadId,
) -> ThreadStoreResult<()>
作用:关闭一个正在写的线程记录器,并把它从内存里的活跃列表移除。它用于线程结束或系统收尾时,避免记录器继续占着资源。
数据流:输入是本地线程仓库和线程编号。它先拿到活跃记录器,调用 shutdown 让记录器完成自己的关闭动作;然后同步实际 rollout 路径;最后锁住 live_recorders 这张内存表,把这个线程编号对应的记录器删掉。输出是成功或错误;成功后,这个线程不再被当作“正在写”。
调用关系:它由外层 shutdown_thread 调用,属于收尾流程。它先把关闭动作交给记录器,再把路径同步交给 sync_materialized_rollout_path,最后自己负责从 store.live_recorders 里清理登记。
调用图:调用 2 个内部函数(live_recorder, sync_materialized_rollout_path);被 1 处调用(shutdown_thread)。
discard_thread132–143 ↗
async fn discard_thread(
store: &LocalThreadStore,
thread_id: ThreadId,
) -> ThreadStoreResult<()>
作用:直接丢弃一个活跃线程记录器的内存登记。它不做保存或关闭动作,只是把它从“正在写”的表里拿掉。
数据流:输入是本地线程仓库和线程编号。它锁住 live_recorders 这张内存表,尝试删除对应记录器;删到了就返回成功,没找到就返回 ThreadNotFound。输出是成功或“线程不存在”错误;它改动的是内存中的活跃记录器列表。
调用关系:它由外层 discard_thread 调用,适合不想继续保留这个活跃记录器的场景。和 shutdown_thread 不同,它没有调用记录器的 shutdown,也没有同步数据库路径,是一个更直接的内存清理动作。
调用图:被 1 处调用(discard_thread)。
rollout_path145–157 ↗
async fn rollout_path(
store: &LocalThreadStore,
thread_id: ThreadId,
) -> ThreadStoreResult<PathBuf>
作用:查询某个活跃线程当前正在使用的 rollout 文件路径。外部需要知道“这场线程流水账写在哪个文件里”时会用它。
数据流:输入是本地线程仓库和线程编号。它锁住活跃记录器表,找到对应记录器,读取记录器里的 rollout_path,并复制成 PathBuf 返回;如果没有这个线程,就返回 ThreadNotFound。输出是路径或错误;它只读取信息,不修改状态。
调用关系:它是很多流程的公共查询小工具。live_rollout_path、load_history、resolve_rollout_path、apply_metadata_update、update_thread_metadata 等流程会用它拿到活跃线程的文件位置;sync_materialized_rollout_path 也会先通过它找到实际路径,再决定是否更新数据库。
调用图:被 7 处调用(live_rollout_path, load_history, sync_materialized_rollout_path, resolve_rollout_path, apply_metadata_update, resolve_rollout_path, update_thread_metadata)。
sync_materialized_rollout_path159–200 ↗
async fn sync_materialized_rollout_path(
store: &LocalThreadStore,
thread_id: ThreadId,
) -> ThreadStoreResult<()>
作用:把“实际已经生成的 rollout 文件路径”和数据库里记录的路径对齐。它防止数据库还指着旧路径,导致以后恢复线程时找错文件。
数据流:输入是本地线程仓库和线程编号。它先用 rollout_path 拿到活跃记录器的路径,再检查这个路径对应的实际 rollout 文件是否已经存在;如果文件还没真正生成,或者仓库没有状态数据库,就直接成功返回。否则它读取数据库里的线程元数据,比较里面的 rollout_path 和实际路径;不一致时就更新数据库。输出始终尽量返回成功;如果同步过程中出错,它会写一条警告日志,但不会把这个错误继续抛出去。
调用关系:它是 persist_thread、flush_thread 和 shutdown_thread 之后的收尾校准器。它会调用 state_db 拿数据库连接,调用 existing_rollout_path 确认文件真的存在,必要时调用数据库的 get_thread 和 upsert_thread。出错时用 warn! 记录警告,因为路径同步失败不应该打断主要的保存或关闭流程。
调用图:调用 2 个内部函数(state_db, rollout_path);被 3 处调用(flush_thread, persist_thread, shutdown_thread);外部调用 2 个(existing_rollout_path, warn!)。
thread_store_io_error202–206 ↗
fn thread_store_io_error(err: std::io::Error) -> ThreadStoreError
作用:把普通的磁盘输入输出错误转换成线程存储模块统一使用的错误格式。这样上层不用分别理解各种底层错误类型。
数据流:输入是 std::io::Error,也就是读写文件时可能产生的底层错误。它把错误转成字符串,放进 ThreadStoreError::Internal 的 message 里。输出是统一的 ThreadStoreError;它不修改任何外部状态。
调用关系:它是本文件里多个写盘动作的错误翻译器。append_items、persist_thread、flush_thread、shutdown_thread 等调用记录器方法时,如果遇到输入输出错误,就通过它把错误变成线程仓库能统一返回的形式。
调用图:外部调用 1 个(to_string)。
thread-store/src/local/read_thread.rs源码 ↗
可以把这里看成“会话档案室的取档员”。线程信息有两份来源:SQLite 像一张索引卡,读起来快,里面有标题、时间、模型等摘要;rollout 文件像完整档案,记录了真实会话内容和历史。这个文件先尽量用 SQLite 快速定位和补全信息,但如果要读完整历史,或者发现 SQLite 里的文件路径已经失效、指向别的线程,就会回到 rollout 文件重新查。它还会处理归档线程:默认不让普通读取看到归档内容,除非调用方明确说要包含归档。读取时还会补上标题、Git 信息、权限配置、父子线程关系,以及可选的完整历史。这样上层代码只要说“我要这个线程”,不用自己知道数据到底藏在数据库还是文件里。
read_thread29–86 ↗
async fn read_thread(
store: &LocalThreadStore,
params: ReadThreadParams,
) -> ThreadStoreResult<StoredThread>
作用:按线程 ID 读取一个线程,是这个文件最主要的入口。它会决定优先用 SQLite 里的摘要,还是去 rollout 文件里找真实记录,并按参数决定是否允许归档和是否加载完整历史。
数据流:输入是本地存储对象和读取参数,参数里有线程 ID、是否包含归档、是否包含历史。它先查 SQLite 元数据,确认这份元数据没有被归档规则排除、路径还能用于加载历史;能用就转成 StoredThread,必要时再用 rollout 文件补更准确的预览和工作目录。不能用时,它会查找 rollout 文件路径,再从文件读线程。最后如果请求了历史,就把历史挂到结果上;成功返回 StoredThread,失败返回明确的线程存储错误。
调用关系:上层的读取、恢复、加载历史和更新元数据流程会调用它。它自己把工作分给 read_sqlite_metadata、stored_thread_from_sqlite_metadata、resolve_rollout_path、read_thread_from_rollout_path 和 attach_history_if_requested;其中 sqlite_rollout_path_can_load_history_for_thread 专门帮它判断 SQLite 里的路径是否还可信。
调用图:调用 8 个内部函数(permission_profile_from_metadata_value, rollout_path_is_archived, attach_history_if_requested, read_sqlite_metadata, read_thread_from_rollout_path, resolve_rollout_path, sqlite_rollout_path_can_load_history_for_thread, stored_thread_from_sqlite_metadata);被 5 处调用(load_history, read_thread, resume_thread, apply_metadata_update, update_thread_metadata);外部调用 1 个(format!)。
sqlite_rollout_path_can_load_history_for_thread88–102 ↗
async fn sqlite_rollout_path_can_load_history_for_thread(
store: &LocalThreadStore,
path: &std::path::Path,
thread_id: codex_protocol::ThreadId,
) -> bool
作用:检查 SQLite 里保存的 rollout 文件路径,是否真的还能用来加载这个线程的历史。它防止数据库里留下旧路径后,把别的文件当成当前线程来读。
数据流:输入是存储对象、一个路径和线程 ID。它先确认文件还存在,再尝试按这个路径读出线程摘要,并比较读出的线程 ID 是否和目标一致。输出是布尔值:true 表示可以信,false 表示不能用。
调用关系:它只被 read_thread 在请求完整历史时使用。它会调用 read_thread_from_rollout_path 做一次轻量验证,因为完整历史必须从 rollout 文件回放,不能只相信 SQLite 摘要。
调用图:调用 1 个内部函数(read_thread_from_rollout_path);被 1 处调用(read_thread);外部调用 2 个(to_path_buf, existing_rollout_path)。
read_thread_by_rollout_path104–135 ↗
async fn read_thread_by_rollout_path(
store: &LocalThreadStore,
rollout_path: std::path::PathBuf,
include_archived: bool,
include_history: bool,
) -> ThreadStoreResult<StoredThread>
作用:按指定的 rollout 文件路径读取线程,而不是按线程 ID 搜索。适合调用方已经知道会话文件在哪里的场景。
数据流:输入是存储对象、rollout 路径、是否包含归档、是否包含历史。它先把相对路径或不规范路径解析成真实文件路径,再从文件读出线程;如果不允许归档却读到归档线程,就返回错误。随后它会查 SQLite,看有没有更新的 Git 信息可以覆盖或补齐,最后按需加载历史并返回线程。
调用关系:上层按路径读取、加载历史和更新元数据时会走这里。它依次使用 resolve_requested_rollout_path、read_thread_from_rollout_path、read_sqlite_metadata、git_info_from_parts 和 attach_history_if_requested,把路径校验、文件读取、数据库补充和历史加载串起来。
调用图:调用 5 个内部函数(git_info_from_parts, attach_history_if_requested, read_sqlite_metadata, read_thread_from_rollout_path, resolve_requested_rollout_path);被 4 处调用(load_history, read_thread_by_rollout_path, read_thread_by_rollout_path_params, update_thread_metadata);外部调用 1 个(format!)。
resolve_requested_rollout_path137–176 ↗
async fn resolve_requested_rollout_path(
store: &LocalThreadStore,
rollout_path: std::path::PathBuf,
) -> ThreadStoreResult<std::path::PathBuf>
作用:把调用方给的 rollout 路径变成一个确定存在的真实文件路径。它会拒绝目录、非普通文件和不存在的文件,避免后面读错东西。
数据流:输入是存储对象和一个路径。相对路径会先拼到 codex_home 下面;然后检查这个路径是不是目录、是不是普通文件、文件是否真的存在;最后转成系统认可的标准路径。输出是可安全读取的 PathBuf,或者返回说明原因的错误。
调用关系:它只服务 read_thread_by_rollout_path。它把路径安全检查提前做好,让 read_thread_from_rollout_path 可以专心解析线程内容。
调用图:被 1 处调用(read_thread_by_rollout_path);外部调用 5 个(is_relative, existing_rollout_path, format!, canonicalize, metadata)。
attach_history_if_requested178–194 ↗
async fn attach_history_if_requested(
thread: &mut StoredThread,
include_history: bool,
) -> ThreadStoreResult<()>
作用:根据调用方要求,决定是否把完整会话历史塞进线程结果里。没有请求历史时它什么也不做,避免不必要地读取大文件。
数据流:输入是一个可修改的 StoredThread 和 include_history 标志。如果标志为 false,线程保持原样;如果为 true,它从线程里的 rollout_path 找到文件,读取所有历史条目,然后把 StoredThreadHistory 放到 thread.history 中。缺少路径或读取失败时返回错误。
调用关系:read_thread 和 read_thread_by_rollout_path 都会在最后调用它。它把真正读历史的工作交给 load_history_items,自己负责判断是否需要读以及把结果挂回线程。
调用图:调用 1 个内部函数(load_history_items);被 2 处调用(read_thread, read_thread_by_rollout_path);外部调用 1 个(format!)。
resolve_rollout_path196–243 ↗
async fn resolve_rollout_path(
store: &LocalThreadStore,
thread_id: codex_protocol::ThreadId,
include_archived: bool,
) -> ThreadStoreResult<Option<std::path::PathBuf>>
作用:按线程 ID 找到对应的 rollout 文件路径。它既会优先看正在写入或常规位置的文件,也能在需要时查归档目录。
数据流:输入是存储对象、线程 ID、是否包含归档。它先问 live_writer 有没有当前线程的文件路径,并确认文件存在且没有被归档规则排除;找不到时再借助状态数据库上下文到 rollout 索引中搜索。若允许归档,会先找普通文件再找归档文件;不允许归档则只找普通文件。输出是可选路径,或定位失败错误。
调用关系:它只被 read_thread 使用,是按线程 ID 读文件前的寻路员。它会调用 live_writer::rollout_path、find_thread_path_by_id_str、find_archived_thread_path_by_id_str 和 rollout_path_is_archived,把各种可能位置统一成一个路径结果。
调用图:调用 3 个内部函数(state_db, rollout_path_is_archived, rollout_path);被 1 处调用(read_thread);外部调用 4 个(existing_rollout_path, find_archived_thread_path_by_id_str, find_thread_path_by_id_str, to_string)。
read_thread_from_rollout_path245–279 ↗
async fn read_thread_from_rollout_path(
store: &LocalThreadStore,
path: std::path::PathBuf,
) -> ThreadStoreResult<StoredThread>
作用:从一个 rollout 文件里读出线程摘要。rollout 文件是会话的原始记录,它比 SQLite 摘要更接近真实对话内容。
数据流:输入是存储对象和 rollout 文件路径。它先尝试从文件中读出能代表线程的条目;如果文件里只有会话头信息、没有用户预览,就退回到 stored_thread_from_session_meta。读到条目后,它会判断是否归档,转换成 StoredThread,补上规范路径、分叉来源、父线程、模型提供方和旧版标题。输出是整理好的线程,或读取失败错误。
调用关系:read_thread、read_thread_by_rollout_path 和 sqlite_rollout_path_can_load_history_for_thread 都会调用它。它把转换工作交给 stored_thread_from_rollout_item,必要时交给 stored_thread_from_session_meta,并用 set_thread_name_from_title 补旧标题。
调用图:调用 4 个内部函数(rollout_path_is_archived, set_thread_name_from_title, stored_thread_from_rollout_item, stored_thread_from_session_meta);被 3 处调用(read_thread, read_thread_by_rollout_path, sqlite_rollout_path_can_load_history_for_thread);外部调用 6 个(as_path, clone, find_thread_name_by_id, plain_rollout_path, read_session_meta_line, read_thread_item_from_rollout)。
load_history_items281–290 ↗
async fn load_history_items(
path: &std::path::Path,
) -> ThreadStoreResult<Vec<codex_protocol::protocol::RolloutItem>>
作用:读取一个 rollout 文件里的完整历史条目。它用于把会话从“摘要”升级成“完整记录”。
数据流:输入是 rollout 文件路径。它调用 RolloutRecorder 读取文件里的所有 RolloutItem,并把读取错误包装成线程存储错误。输出是历史条目列表。
调用关系:它只被 attach_history_if_requested 调用。attach_history_if_requested 决定要不要读历史,它负责真正把文件内容加载出来。
调用图:调用 1 个内部函数(load_rollout_items);被 1 处调用(attach_history_if_requested)。
read_sqlite_metadata292–298 ↗
async fn read_sqlite_metadata(
store: &LocalThreadStore,
thread_id: codex_protocol::ThreadId,
) -> Option<ThreadMetadata>
作用:从 SQLite 状态数据库里按线程 ID 取摘要元数据。SQLite 是一个本地小数据库,用来快速保存和查询线程索引信息。
数据流:输入是存储对象和线程 ID。它先拿到状态数据库运行时;如果没有数据库就直接返回空。拿到后查询这个线程,成功且存在时返回 ThreadMetadata,否则返回空。
调用关系:read_thread 和 read_thread_by_rollout_path 会调用它。它提供快速摘要来源,但调用方会再判断这份摘要是否足够可信。
调用图:调用 1 个内部函数(state_db);被 2 处调用(read_thread, read_thread_by_rollout_path)。
stored_thread_from_sqlite_metadata300–362 ↗
async fn stored_thread_from_sqlite_metadata(
store: &LocalThreadStore,
metadata: ThreadMetadata,
) -> StoredThread
作用:把 SQLite 里的 ThreadMetadata 摘要转换成上层统一使用的 StoredThread。它负责把数据库字段翻译成展示和业务都能用的线程对象。
数据流:输入是存储对象和一条 ThreadMetadata。它会挑选标题,读取 session_meta 补分叉和父线程信息,规范 rollout 路径,选择预览文本,解析来源、审批模式和权限配置,再整理 Git 信息、模型、时间、工作目录等字段。输出是不带完整历史的 StoredThread。
调用关系:它被 read_thread 在采用 SQLite 摘要路线时调用。它内部会用 distinct_thread_metadata_title、find_thread_name_by_id、read_session_meta_line、parse_session_source、parse_or_default、permission_profile_from_metadata_value 和 git_info_from_parts,把零散字段组装成统一对象。
调用图:调用 5 个内部函数(distinct_thread_metadata_title, git_info_from_parts, permission_profile_from_metadata_value, parse_or_default, parse_session_source);被 1 处调用(read_thread);外部调用 3 个(find_thread_name_by_id, plain_rollout_path, read_session_meta_line)。
stored_thread_from_session_meta364–377 ↗
async fn stored_thread_from_session_meta(
store: &LocalThreadStore,
path: std::path::PathBuf,
) -> ThreadStoreResult<StoredThread>
作用:当 rollout 文件里没有可用的对话摘要时,只根据文件开头的 session_meta 会话头信息创建线程。它是一个兜底方案。
数据流:输入是存储对象和文件路径。它读取 session_meta 行,判断这个路径是否归档,然后调用 stored_thread_from_meta_line 生成 StoredThread。读取失败会返回内部错误。
调用关系:它只被 read_thread_from_rollout_path 在读不到常规 rollout 条目时调用。它把具体建对象的工作交给 stored_thread_from_meta_line。
调用图:调用 2 个内部函数(rollout_path_is_archived, stored_thread_from_meta_line);被 1 处调用(read_thread_from_rollout_path);外部调用 2 个(as_path, read_session_meta_line)。
stored_thread_from_meta_line379–424 ↗
fn stored_thread_from_meta_line(
store: &LocalThreadStore,
meta_line: SessionMetaLine,
path: std::path::PathBuf,
archived: bool,
) -> StoredThread
作用:把一条 session_meta 会话头记录转换成 StoredThread。这个结果通常没有预览文本,但能保留线程 ID、时间、工作目录、来源等基础信息。
数据流:输入是存储对象、session_meta 行、文件路径和是否归档。它解析创建时间,读取文件修改时间作为更新时间,规范路径,填入线程 ID、父子关系、模型提供方、工作目录、CLI 版本、来源、Git 信息等字段;如果是归档文件,就设置 archived_at。输出是不含历史、权限默认只读的 StoredThread。
调用关系:它由 stored_thread_from_session_meta 调用。它会用 parse_rfc3339_non_optional 解析时间,用 plain_rollout_path 保存标准路径,并用 PermissionProfile::read_only 作为安全默认值。
调用图:调用 2 个内部函数(read_only, parse_rfc3339_non_optional);被 1 处调用(stored_thread_from_session_meta);外部调用 4 个(as_path, new, plain_rollout_path, metadata)。
parse_session_source426–430 ↗
fn parse_session_source(source: &str) -> SessionSource
作用:把数据库里保存的来源字符串转换成 SessionSource 类型。来源表示线程是从命令行、执行环境还是未知地方来的。
数据流:输入是一个字符串。它先按 JSON 格式解析;如果不是 JSON,就当普通字符串再解析一次;两种都失败时返回 Unknown。输出是一个安全可用的 SessionSource。
调用关系:它被 stored_thread_from_sqlite_metadata 调用,用来兼容新旧两种保存格式,避免旧数据让读取失败。
调用图:被 1 处调用(stored_thread_from_sqlite_metadata);外部调用 1 个(from_str)。
parse_or_default432–439 ↗
fn parse_or_default(value: &str, default: T) -> T
作用:通用的小工具:把字符串解析成某个类型,失败就给默认值。它用来读取那些可能有新旧格式差异的数据库字段。
数据流:输入是字符串和默认值。它先按 JSON 解析;失败后把字符串当普通 JSON 字符串再解析;如果还是失败,就返回传入的默认值。输出是目标类型的值。
调用关系:它被 stored_thread_from_sqlite_metadata 用来解析审批模式等字段。这样元数据格式稍有变化时,线程读取仍能继续。
调用图:被 1 处调用(stored_thread_from_sqlite_metadata);外部调用 1 个(from_str)。
parse_rfc3339_non_optional441–445 ↗
fn parse_rfc3339_non_optional(value: &str) -> Option<DateTime<Utc>>
作用:把 RFC3339 格式的时间字符串转成 UTC 时间。RFC3339 是一种常见时间写法,比如 2025-01-03T12:00:00Z。
数据流:输入是时间字符串。它尝试解析,成功后统一转成 UTC 时区;失败则返回空。输出是可选的 DateTime<Utc>。
调用关系:它被 stored_thread_from_meta_line 用来解析 session_meta 里的创建时间。解析失败时,上层会改用当前时间兜底。
调用图:被 1 处调用(stored_thread_from_meta_line);外部调用 1 个(parse_from_rfc3339)。
tests::read_thread_returns_active_rollout_summary470–495 ↗
async fn read_thread_returns_active_rollout_summary()
作用:测试普通未归档 rollout 文件能被按线程 ID 正常读出,并且请求历史时会带上历史。
数据流:测试先创建临时目录、存储对象和一份会话文件。然后调用 read_thread,检查线程 ID、路径、归档状态、预览文本和历史线程 ID 都符合预期。
调用关系:这是 read_thread 的基础成功用例。它通过 write_session_file 准备文件,间接覆盖 resolve_rollout_path、read_thread_from_rollout_path 和 attach_history_if_requested。
调用图:调用 4 个内部函数(from_string, new, test_config, write_session_file);外部调用 3 个(new, from_u128, assert_eq!)。
tests::read_thread_returns_rollout_path_summary498–525 ↗
async fn read_thread_returns_rollout_path_summary()
作用:测试可以用相对 rollout 路径直接读取线程摘要。
数据流:测试创建会话文件后,把文件路径改成相对 codex_home 的路径传入 read_thread_by_rollout_path。结果应返回正确线程 ID、标准化后的文件路径和预览文本。
调用关系:它验证 read_thread_by_rollout_path 会先调用 resolve_requested_rollout_path 解析相对路径,再读取 rollout 内容。
调用图:调用 4 个内部函数(from_string, new, test_config, write_session_file);外部调用 3 个(new, from_u128, assert_eq!)。
tests::read_thread_by_rollout_path_prefers_sqlite_git_info528–574 ↗
async fn read_thread_by_rollout_path_prefers_sqlite_git_info()
作用:测试按路径读取时,如果 SQLite 里有更新的 Git 信息,会优先使用 SQLite 的值并保留文件里的其他 Git 字段。
数据流:测试创建 rollout 文件和 SQLite 元数据,其中 SQLite 写入特定分支名。读取后检查 Git 分支来自 SQLite,而提交哈希和仓库地址仍能从 rollout 补齐。
调用关系:它覆盖 read_thread_by_rollout_path 里读取 SQLite 后用 git_info_from_parts 合并 Git 信息的流程。
调用图:调用 6 个内部函数(from_string, new, init, new, test_config, write_session_file);外部调用 4 个(new, now, from_u128, assert_eq!)。
tests::read_thread_returns_archived_rollout_when_requested577–615 ↗
async fn read_thread_returns_archived_rollout_when_requested()
作用:测试归档线程默认读不到,但明确允许归档时可以读到。
数据流:测试只创建归档会话文件。第一次读取不包含归档,应得到找不到线程的错误;第二次包含归档,应返回线程路径、归档时间和预览文本,且不带历史。
调用关系:它验证 read_thread、resolve_rollout_path 和归档过滤逻辑配合正确。
调用图:调用 4 个内部函数(from_string, new, test_config, write_archived_session_file);外部调用 5 个(new, from_u128, assert!, assert_eq!, panic!)。
tests::read_thread_prefers_active_rollout_over_archived618–640 ↗
async fn read_thread_prefers_active_rollout_over_archived()
作用:测试同一个线程同时有活跃文件和归档文件时,即使允许归档,也优先读活跃文件。
数据流:测试为同一线程创建一份活跃会话和一份归档会话。读取时允许归档,结果应指向活跃路径,归档时间为空,预览来自活跃文件。
调用关系:它验证 resolve_rollout_path 搜索顺序:先找普通线程,再考虑归档线程。
调用图:调用 5 个内部函数(from_string, new, test_config, write_archived_session_file, write_session_file);外部调用 3 个(new, from_u128, assert_eq!)。
tests::read_thread_returns_forked_from_id643–672 ↗
async fn read_thread_returns_forked_from_id()
作用:测试从分叉会话文件读取时,能保留它是从哪个父线程分叉来的。
数据流:测试创建带 fork 信息的会话文件。读取后检查返回的 forked_from_id 等于父线程 ID。
调用关系:它覆盖 read_thread_from_rollout_path 读取 session_meta 并补充分叉关系的逻辑。
调用图:调用 4 个内部函数(from_string, new, test_config, write_session_file_with_fork);外部调用 3 个(new, from_u128, assert_eq!)。
tests::read_thread_applies_sqlite_thread_name675–712 ↗
async fn read_thread_applies_sqlite_thread_name()
作用:测试 SQLite 里保存的标题会成为线程名称。
数据流:测试创建 rollout 文件和 SQLite 元数据,并在元数据里写入 Saved title。读取线程后检查 name 字段就是这个标题。
调用关系:它验证 read_thread 采用 SQLite 元数据路线时,stored_thread_from_sqlite_metadata 会使用 distinct_thread_metadata_title 得到名称。
调用图:调用 6 个内部函数(from_string, new, init, new, test_config, write_session_file);外部调用 4 个(new, now, from_u128, assert_eq!)。
tests::read_thread_returns_permission_profile_from_sqlite_metadata715–753 ↗
async fn read_thread_returns_permission_profile_from_sqlite_metadata()
作用:测试 SQLite 元数据里的权限配置会被正确读出来。权限配置决定线程能读写哪些东西。
数据流:测试把 PermissionProfile::Disabled 序列化后写入 SQLite 的 sandbox_policy 字段。读取线程后检查预览来自 rollout,权限配置来自 SQLite。
调用关系:它覆盖 read_thread 在用 rollout 补摘要时仍保留 SQLite 权限信息,并通过 permission_profile_from_metadata_value 转换。
调用图:调用 6 个内部函数(from_string, new, init, new, test_config, write_session_file);外部调用 5 个(new, now, from_u128, assert_eq!, to_string)。
tests::read_thread_accepts_legacy_sandbox_policy_metadata756–791 ↗
async fn read_thread_accepts_legacy_sandbox_policy_metadata()
作用:测试旧格式的沙箱策略字符串仍能被识别。沙箱策略可以理解为限制程序访问范围的一套安全规则。
数据流:测试把旧字符串 danger-full-access 写进 SQLite 元数据。读取并加载历史后,检查它被转换成对应的 PermissionProfile::Disabled。
调用关系:它验证 permission_profile_from_metadata_value 对旧数据兼容,也覆盖 read_thread 加载历史时的 SQLite 路线。
调用图:调用 6 个内部函数(from_string, new, init, new, test_config, write_session_file);外部调用 4 个(new, now, from_u128, assert_eq!)。
tests::read_thread_preserves_rollout_cwd_when_sqlite_metadata_exists794–881 ↗
async fn read_thread_preserves_rollout_cwd_when_sqlite_metadata_exists()
作用:测试当 SQLite 和 rollout 都有工作目录时,读取摘要会保留 rollout 文件里的真实工作目录。
数据流:测试手写一个 rollout 文件,里面的 cwd 是根目录,同时 SQLite 里写另一个 cwd 和标题。读取后检查预览、模型提供方和 cwd 来自 rollout,标题来自 SQLite,权限配置按 rollout 的 cwd 解释。
调用关系:它覆盖 read_thread 先用 SQLite 但再用 rollout 补真实摘要的分支,特别是权限配置需要结合最终工作目录重新计算。
调用图:调用 5 个内部函数(from_string, new, init, new, test_config);外部调用 11 个(from, new, now, from_u128, new, assert_eq!, format!, json!, create, create_dir_all (+1 more))。
tests::read_thread_uses_legacy_thread_name_when_sqlite_title_is_missing884–904 ↗
async fn read_thread_uses_legacy_thread_name_when_sqlite_title_is_missing()
作用:测试 SQLite 没有标题时,可以使用旧版线程名称记录。
数据流:测试创建会话文件后,通过旧接口追加 Legacy title。读取线程后检查 name 字段使用这个旧标题。
调用关系:它验证 read_thread_from_rollout_path 会调用 find_thread_name_by_id 并通过 set_thread_name_from_title 补名称。
调用图:调用 4 个内部函数(from_string, new, test_config, write_session_file);外部调用 4 个(new, from_u128, assert_eq!, append_thread_name)。
tests::read_thread_uses_sqlite_metadata_for_rollout_without_user_preview907–973 ↗
async fn read_thread_uses_sqlite_metadata_for_rollout_without_user_preview()
作用:测试 rollout 文件没有用户消息预览时,仍能用 SQLite 摘要返回有用信息。
数据流:测试手写一个只有 session_meta 的 rollout 文件,再写入 SQLite 元数据,包括标题、模型提供方、工作目录和 CLI 版本。读取并请求历史后,检查预览为空但其他摘要字段来自 SQLite,历史包含文件里的记录。
调用关系:它覆盖 read_thread 在请求历史时确认 SQLite 路径可信,然后使用 stored_thread_from_sqlite_metadata,并通过 attach_history_if_requested 加载历史。
调用图:调用 5 个内部函数(from_string, new, init, new, test_config);外部调用 9 个(new, now, from_u128, assert_eq!, format!, json!, create, create_dir_all, writeln!)。
tests::read_thread_falls_back_to_rollout_search_when_sqlite_path_is_stale976–1022 ↗
async fn read_thread_falls_back_to_rollout_search_when_sqlite_path_is_stale()
作用:测试 SQLite 里的 rollout 路径不存在时,会放弃这条旧索引,重新搜索真实文件。
数据流:测试创建真实会话文件,但 SQLite 元数据指向外部不存在的路径。读取并请求历史后,结果应来自真实 rollout 文件,而不是过期 SQLite 摘要。
调用关系:它验证 sqlite_rollout_path_can_load_history_for_thread 会发现路径失效,使 read_thread 回落到 resolve_rollout_path 搜索。
调用图:调用 6 个内部函数(from_string, new, init, new, test_config, write_session_file);外部调用 4 个(new, now, from_u128, assert_eq!)。
tests::read_thread_falls_back_when_sqlite_path_points_to_another_thread1025–1069 ↗
async fn read_thread_falls_back_when_sqlite_path_points_to_another_thread()
作用:测试 SQLite 路径虽然存在,但如果指向另一个线程,也不能被当前线程使用。
数据流:测试创建当前线程文件,又创建另一个线程文件,并让 SQLite 元数据错误指向另一个线程。读取并请求历史后,结果应来自当前线程的正确 rollout 文件。
调用关系:它验证 sqlite_rollout_path_can_load_history_for_thread 不只检查文件存在,还会读出线程 ID 做匹配。
调用图:调用 6 个内部函数(from_string, new, init, new, test_config, write_session_file);外部调用 4 个(new, now, from_u128, assert_eq!)。
tests::read_thread_uses_session_meta_for_rollout_without_user_preview_or_sqlite_metadata1072–1122 ↗
async fn read_thread_uses_session_meta_for_rollout_without_user_preview_or_sqlite_metadata()
作用:测试没有 SQLite 元数据、rollout 也没有用户预览时,可以只靠 session_meta 读出基础线程。
数据流:测试手写一个只有 session_meta 的文件。读取并请求历史后,检查线程 ID、时间、工作目录、CLI 版本、来源、模型提供方等基础字段正确,预览为空,历史有一条。
调用关系:它覆盖 read_thread_from_rollout_path 退到 stored_thread_from_session_meta,再由 stored_thread_from_meta_line 构造线程的兜底路线。
调用图:调用 3 个内部函数(from_string, new, test_config);外部调用 9 个(new, from_u128, assert!, assert_eq!, format!, json!, create, create_dir_all, writeln!)。
tests::read_thread_falls_back_to_sqlite_summary1125–1184 ↗
async fn read_thread_falls_back_to_sqlite_summary()
作用:测试在没有可用 rollout 文件且不需要历史时,可以直接返回 SQLite 摘要。
数据流:测试让 SQLite 元数据指向外部路径,但不创建对应文件,并写入预览、标题、模型、工作目录等字段。读取时不请求历史,结果应来自 SQLite 摘要且不带历史。
调用关系:它验证 read_thread 在不需要完整历史时,可以把 SQLite 当作足够的信息来源,而不强制要求 rollout 文件存在。
调用图:调用 5 个内部函数(from_string, new, init, new, test_config);外部调用 6 个(new, now, from_u128, assert!, assert_eq!, format!)。
tests::read_thread_sqlite_fallback_respects_include_archived1187–1241 ↗
async fn read_thread_sqlite_fallback_respects_include_archived()
作用:测试只靠 SQLite 摘要返回线程时,也必须遵守是否包含归档的开关。
数据流:测试写入一条 archived_at 不为空的 SQLite 元数据。第一次不允许归档应报找不到;第二次允许归档应返回线程,并带有归档时间。
调用关系:它验证 read_thread 对 SQLite 元数据和 rollout 文件使用同一套归档可见性规则。
调用图:调用 5 个内部函数(from_string, new, init, new, test_config);外部调用 7 个(new, now, from_u128, assert!, assert_eq!, format!, panic!)。
tests::read_thread_sqlite_fallback_loads_archived_history1244–1288 ↗
async fn read_thread_sqlite_fallback_loads_archived_history()
作用:测试归档线程如果有 SQLite 摘要和归档 rollout 文件,在允许归档并请求历史时可以正常加载历史。
数据流:测试创建归档会话文件,并写入指向它的 SQLite 元数据。读取时包含归档且包含历史,结果应有归档状态、SQLite 预览和完整历史条目。
调用关系:它覆盖 read_thread 采用 SQLite 摘要、验证归档路径可用于历史、再调用 attach_history_if_requested 的组合流程。
调用图:调用 6 个内部函数(from_string, new, init, new, test_config, write_archived_session_file);外部调用 5 个(new, now, from_u128, assert!, assert_eq!)。
tests::read_thread_fails_without_rollout1291–1313 ↗
async fn read_thread_fails_without_rollout()
作用:测试找不到任何 rollout 文件、也没有可用 SQLite 摘要时,读取会明确失败。
数据流:测试只创建存储环境和线程 ID,不创建会话文件。调用 read_thread 后应得到 InvalidRequest 错误,消息说明找不到这个线程的 rollout。
调用关系:它验证 read_thread 在 resolve_rollout_path 返回空时的错误路径,防止静默返回空对象或误报成功。
调用图:调用 3 个内部函数(from_string, new, test_config);外部调用 4 个(new, from_u128, assert_eq!, panic!)。
thread-store/src/local/list_threads.rs源码 ↗
这个文件解决的是“怎么可靠地列出本地对话记录”的问题。对话信息可能在两种地方:旧的会话文件里,或新的状态数据库里。它先检查用户传来的分页游标(游标就是“从上次看到的位置继续往后翻”的标记),再把排序方式、筛选条件翻译成底层读取器能懂的格式。真正取数据时,它会根据参数决定读普通线程、归档线程、只读数据库,还是按父线程筛选。拿到结果后,它还会做一层整理:把底层格式变成线程存储模块统一的格式,并尽量给每个线程补上好看的标题。标题优先从状态数据库拿,如果数据库没有,再去旧文件里找。可以把它理解成历史记录页面前面的“服务员”:后厨可能有多个仓库,但它负责把菜单按用户要求整理好端出来。
list_threads21–108 ↗
async fn list_threads(
store: &LocalThreadStore,
params: ListThreadsParams,
) -> ThreadStoreResult<ThreadPage>
作用:这是列出本地线程的主入口。有人要看历史对话列表时,会走到这里,它负责把请求参数检查好、交给底层读取数据,再把结果整理成统一的页面返回。
数据流:进去的是本地线程仓库 store 和列表参数 params,包括页大小、游标、排序、是否归档、搜索词等。它先解析游标,游标不合法就返回“请求无效”;再取得状态数据库句柄和本地配置,调用 list_rollout_threads 去拿原始线程页。拿到后,它把下一页游标转成字符串,把每条原始记录转成统一的线程对象;然后收集线程 ID,先从状态数据库查标题,再用 find_thread_names_by_ids 从旧文件里补标题,最后用 set_thread_name_from_title 写回线程对象。出来的是 ThreadPage,也就是一页线程列表和可能存在的下一页游标。
调用关系:它处在本地线程列表流程的最外层,通常由 LocalThreadStore 的 list_threads 能力触发。它自己不直接扫所有文件或数据库,而是把“该去哪儿取线程”的决定交给 list_rollout_threads;拿到结果后,再调用 distinct_thread_metadata_title、set_thread_name_from_title 和 find_thread_names_by_ids 做标题补全。调用图里显示它也被 list_threads 调用,这通常代表上层同名方法把工作转交给这个文件里的具体实现。
调用图:调用 4 个内部函数(state_db, distinct_thread_metadata_title, set_thread_name_from_title, list_rollout_threads);被 1 处调用(list_threads);外部调用 2 个(with_capacity, find_thread_names_by_ids)。
list_rollout_threads110–209 ↗
async fn list_rollout_threads(
state_db: Option<codex_rollout::StateDbHandle>,
config: &RolloutConfig,
default_model_provider_id: &str,
params: &ListThreadsParams,
cursor: Option<&
作用:这个函数专门决定“到底从哪里列线程”。它根据参数选择读状态数据库、读旧的 rollout 会话记录、读归档区,或者按父线程只从数据库筛选。
数据流:进去的是可选的状态数据库句柄、rollout 配置、默认模型提供方、列表参数、分页游标、排序字段和排序方向。它先看有没有 parent_thread_id;如果有,就调用 list_threads_db 只从数据库查这个父线程下面的线程,数据库不可用就报内部错误,并把返回项都标上父线程 ID。没有父线程筛选时,它再看 use_state_db_only 和 archived 两个开关:只读数据库且归档就调用 list_archived_threads_from_state_db,只读数据库且未归档就调用 list_threads_from_state_db;否则按归档状态调用 list_archived_threads 或 list_threads。出来的是底层的 ThreadsPage;如果底层失败,会包装成 ThreadStoreError::Internal。
调用关系:它是 list_threads 和 search_threads 共用的“取数分流器”。list_threads 把清理好的参数交给它;它再把任务交给 codex_rollout 的 RolloutRecorder 或 state_db::list_threads_db。这样上层不用知道数据到底藏在数据库还是文件里,只管拿分页结果。
调用图:调用 5 个内部函数(list_archived_threads, list_archived_threads_from_state_db, list_threads, list_threads_from_state_db, list_threads_db);被 2 处调用(list_threads, search_threads)。
tests::list_threads_uses_default_provider_when_rollout_omits_provider230–262 ↗
async fn list_threads_uses_default_provider_when_rollout_omits_provider()
作用:这个测试确认:如果旧会话文件里没有写明模型提供方,列表结果会自动使用默认模型提供方。这样历史记录不会因为旧数据缺字段而显示空值或错误值。
数据流:测试先创建临时目录和本地仓库,再用 write_session_file_with 写一条没有 model_provider 的会话文件。然后它调用 store.list_threads 请求第一页普通线程。最后检查返回的唯一线程里,model_provider 被填成测试配置里的 test-provider。
调用关系:它通过 LocalThreadStore::new 和 test_config 搭好一个最小本地环境,用 write_session_file_with 准备旧格式数据,再触发列表流程。它验证的是 list_threads 调用底层读取和转换时,会把默认 provider 正确补进去。
调用图:调用 3 个内部函数(new, test_config, write_session_file_with);外部调用 4 个(new, from_u128, new, assert_eq!)。
tests::list_threads_preserves_sqlite_title_search_results265–330 ↗
async fn list_threads_preserves_sqlite_title_search_results()
作用:这个测试确认:当用状态数据库按标题搜索时,搜到的线程不会在后续整理时丢掉预览文字等信息。它保护的是“数据库搜索结果能原样、正确地显示在列表里”。
数据流:测试创建临时目录、配置和线程 ID,初始化状态数据库,并标记回填完成。接着它手动构造一条线程元数据,把标题设成 needle title,把首条用户消息和预览设成 plain preview,然后写入数据库。之后它用 search_term 为 needle、use_state_db_only 为 true 调用列表。结果应该只包含这个线程,并且 first_user_message 仍然是 plain preview。
调用关系:它搭起真实的 StateRuntime,再通过 LocalThreadStore 调用列表功能。这个测试覆盖 list_rollout_threads 选择 list_threads_from_state_db 的路径,也间接检查 list_threads 后面的格式转换和标题处理没有破坏数据库返回的内容。
调用图:调用 5 个内部函数(from_string, new, init, new, test_config);外部调用 6 个(new, now, from_u128, new, assert_eq!, write)。
tests::list_threads_selects_active_or_archived_collection333–400 ↗
async fn list_threads_selects_active_or_archived_collection()
作用:这个测试确认:请求普通线程时只返回普通线程,请求归档线程时只返回归档线程。它防止历史列表把已归档和未归档的对话混在一起。
数据流:测试先在临时目录里写一条普通会话文件和一条归档会话文件。然后它分别用 archived 为 false 和 true 调用 store.list_threads。最后检查普通列表只含普通线程 ID,归档列表只含归档线程 ID,并确认普通线程没有 archived_at,归档线程的 archived_at 被设置为更新时间。
调用关系:它用 write_session_file 和 write_archived_session_file 准备两类数据,再触发同一个列表入口。它主要验证 list_rollout_threads 会根据 archived 参数走 list_threads 或 list_archived_threads 两条不同路径。
调用图:调用 5 个内部函数(from_string, new, test_config, write_archived_session_file, write_session_file);外部调用 4 个(new, from_u128, new, assert_eq!)。
tests::list_threads_returns_local_rollout_summary403–441 ↗
async fn list_threads_returns_local_rollout_summary()
作用:这个测试确认:从本地会话文件列出的线程摘要是完整的,包括线程 ID、文件路径、预览、模型提供方、命令行版本和来源。它保证历史记录列表展示所需的关键信息都能拿到。
数据流:测试创建临时目录和仓库,写入一条本地会话文件,然后用来源和模型提供方筛选条件调用 store.list_threads。返回后,它检查没有下一页,只有一条线程,并逐项确认线程 ID、rollout 文件路径、预览文字、first_user_message、model_provider、cli_version 和 source 都符合预期。
调用关系:它通过写真实会话文件来走本地 rollout 读取路径。这个测试把 list_threads、list_rollout_threads 以及 stored_thread_from_rollout_item 的配合效果串起来验证,确保最终给 UI 或调用方看的摘要靠谱。
调用图:调用 4 个内部函数(from_string, new, test_config, write_session_file);外部调用 4 个(new, from_u128, assert_eq!, vec!)。
tests::list_threads_rejects_invalid_cursor444–466 ↗
async fn list_threads_rejects_invalid_cursor()
作用:这个测试确认:传入乱写的分页游标时,列表接口会直接拒绝,而不是继续读数据或返回奇怪结果。它保护分页功能不会被坏输入带偏。
数据流:测试创建临时仓库,然后调用 store.list_threads,并把 cursor 设成 not-a-cursor。函数应该返回错误。测试最后检查这个错误属于 ThreadStoreError::InvalidRequest。
调用关系:它专门覆盖 list_threads 一开始解析游标的那段逻辑,也就是 parse_cursor 失败后的错误路径。这样 list_rollout_threads 等后续取数函数不会在无效游标下被误用。
调用图:调用 2 个内部函数(new, test_config);外部调用 3 个(new, new, assert!)。
thread-store/src/local/search_threads.rs源码 ↗
这个文件解决的是一个很实际的问题:用户有很多本地对话记录,想输入一个词,快速找出哪些线程里提到过它。如果没有这层代码,系统可能只能粗糙地搜到文件,没法把结果变成“线程列表”、没法分页,也没法显示标题和命中片段。它的做法像先用金属探测器扫一遍地面:先调用底层搜索工具在 rollout 文件,也就是本地保存的对话内容文件里找关键词;再按用户要求的排序方式去列出线程;然后把“搜到关键词的文件”和“线程列表”对上号,只留下真正匹配的线程。为了让页面能一页页翻,它会生成 cursor(游标,可以理解成“下次从这里继续”的书签)。最后,它还会从状态数据库或旧格式文件里补出线程名字,并把结果包装成带 snippet(命中片段)的搜索结果返回。
search_threads31–169 ↗
async fn search_threads(
store: &LocalThreadStore,
params: SearchThreadsParams,
) -> ThreadStoreResult<ThreadSearchPage>
作用:这是本文件的主函数,用来执行一次完整的线程搜索。有人输入搜索词、分页大小、排序方式等条件后,它负责找内容、筛线程、分页,并返回可展示的搜索结果。
数据流:输入是本地线程仓库和搜索参数。它先检查搜索词是不是空的,空的话直接报“请求不合法”;再把传入的 cursor 解析成系统能理解的书签,把排序字段和排序方向转换成底层 rollout 搜索能用的格式。之后它读取仓库配置和可选的状态数据库,找到当前安装环境里的 rg 命令,也就是 ripgrep 这类快速文本搜索工具。它先用 search_rollout_matches 扫本地对话内容,得到哪些 rollout 文件可能命中关键词;如果一个都没有,就返回空列表。若有命中,它会循环调用 list_rollout_threads 按排序列出线程页,再把每个线程对应的文件和命中文件表对照,必要时用 first_rollout_content_match_snippet 读取第一段命中内容。它多取一点结果来判断后面还有没有下一页,最后截成用户要求的 page_size,生成 next_cursor,把底层条目转换成 StoredThreadSearchResult,并补上线程名。输出是一页 ThreadSearchPage;过程中不会改写线程内容,只会读取文件、读取数据库,并整理返回数据。
调用关系:它处在“用户发起本地线程搜索”这条流程的中心位置;调用图里显示它由 search_threads 这个入口触发,也就是上层收到搜索请求后会来到这里。它自己不亲自扫文件细节,而是把文本查找交给 search_rollout_matches 和 first_rollout_content_match_snippet,把按时间列线程交给 list_rollout_threads,把路径标准化交给 plain_rollout_path。结果整理完后,它再调用 set_thread_search_result_names 给每条结果补标题,让最终返回的数据更适合界面展示。
调用图:调用 4 个内部函数(current, state_db, list_rollout_threads, set_thread_search_result_names);被 1 处调用(search_threads);外部调用 4 个(new, first_rollout_content_match_snippet, plain_rollout_path, search_rollout_matches)。
cursor_from_thread_search_item171–184 ↗
fn cursor_from_thread_search_item(
item: &ThreadSearchItem,
sort_key: ThreadSortKey,
) -> Option<codex_rollout::Cursor>
作用:这个小函数负责给搜索结果生成“下一页从哪里继续”的书签。它会根据当前排序方式,从结果里挑一个合适的时间戳,把它变成 cursor。
数据流:输入是一条搜索中的线程结果和排序字段。若按创建时间排序,它取这条线程的 created_at;若按更新时间排序,它优先取 updated_at,缺失时退回 created_at。拿到时间字符串后,它调用 parse_cursor 把这个时间变成底层能识别的 Cursor。输出是一个可选的 Cursor;如果时间不存在或解析失败,就输出 None,意思是没法生成可靠的下一页书签。
调用关系:它是 search_threads 做分页时用的辅助零件。search_threads 多取到一条结果来判断还有下一页时,会拿最后一条保留下来的结果调用它,生成 next_cursor。它只负责把时间变书签,真正的解析工作交给 parse_cursor。
调用图:外部调用 1 个(parse_cursor)。
set_thread_search_result_names186–218 ↗
async fn set_thread_search_result_names(
store: &LocalThreadStore,
items: &mut [StoredThreadSearchResult],
)
作用:这个函数给搜索结果补上好读的线程标题。搜索本身主要拿到的是线程内容和编号,但用户更需要看到“这段对话叫什么”,所以这里会尽量把名字填进去。
数据流:输入是本地线程仓库和一批可修改的搜索结果。它先从结果里收集所有 thread_id,也就是每个线程的唯一编号,并准备一个编号到标题的表。然后它优先查状态数据库:如果能找到线程元数据,并且 distinct_thread_metadata_title 判断这个标题是有效且不重复的,就记录下来。若还有线程没标题,它再调用 find_thread_names_by_ids 去旧格式的本地文件里找名字,作为补充来源。最后它逐条检查搜索结果,如果找到了标题,就用 set_thread_name_from_title 写回到结果里的线程对象。输出没有单独返回值,但传入的 items 会被更新,变成带标题的搜索结果。
调用关系:它在 search_threads 的最后阶段运行,属于“把机器能懂的结果整理成人能看懂的结果”。它会向 store.state_db 询问新数据库里的标题,也会在需要时向 find_thread_names_by_ids 查询旧文件里的标题;拿到标题后,再交给 set_thread_name_from_title 统一写入线程结构。这样上层界面不必自己再到处查标题。
调用图:调用 3 个内部函数(state_db, distinct_thread_metadata_title, set_thread_name_from_title);被 1 处调用(search_threads);外部调用 3 个(with_capacity, find_thread_names_by_ids, iter)。
thread-store/src/local/delete_thread.rs源码 ↗
这个文件做的是“硬删除”:用户要删除某个线程时,它会先用线程编号去本地目录里找对应的对话记录文件。记录可能在正常 sessions 目录,也可能已经被归档到 archived sessions 目录,所以两边都要查。找到后,它会删除普通 jsonl 文件,也会顺手删除同名的压缩 jsonl.zst 文件。删除前还会检查路径是不是落在允许的会话目录里,并确认文件名真的匹配这个线程编号,避免误删别的文件。一个比较贴心的行为是:如果文件刚找到但随后被别人删掉了,它会当作“已经删好了”,不会报错。等文件删完后,它还会删掉线程名称索引,并把内存里正在记录这个线程的 recorder 清掉。如果一开始根本没找到对应文件,就返回“线程不存在”。
delete_thread23–85 ↗
async fn delete_thread(
store: &LocalThreadStore,
params: DeleteThreadParams,
) -> ThreadStoreResult<()>
作用:这是删除本地线程的主流程。有人请求删除某个线程时,它负责找到这个线程的所有本地记录文件,删掉它们,再清理相关索引和内存记录。
数据流:输入是一个本地存储对象和删除参数,参数里带着 thread_id,也就是线程编号。它先读取存储配置和可选的状态数据库上下文,然后分别去普通会话目录和归档会话目录查这个编号对应的文件;查到的路径会放进列表。接着它逐个调用文件删除函数,删除实际 rollout 文件和可能存在的压缩副本。之后它删除线程名称索引。如果一开始没有找到任何文件,它返回“线程不存在”;如果成功,它还会从 live_recorders 里移除这个线程,最后返回成功。
调用关系:它是整个删除动作的入口性核心,通常由本地线程存储的 delete_thread 接口在收到删除请求时调用。它把“找文件”的工作交给外部的 find_thread_path_by_id_str 和 find_archived_thread_path_by_id_str,把“删具体文件”的工作交给 delete_rollout_file,最后再调用 remove_thread_name_entries 清掉名称索引。
调用图:调用 2 个内部函数(state_db, delete_rollout_file);被 1 处调用(delete_thread);外部调用 5 个(new, find_archived_thread_path_by_id_str, find_thread_path_by_id_str, remove_thread_name_entries, format!)。
delete_rollout_file87–97 ↗
fn delete_rollout_file(
store: &LocalThreadStore,
rollout_path: &Path,
thread_id: codex_protocol::ThreadId,
) -> ThreadStoreResult<bool>
作用:这个函数负责删除一个线程记录文件的两个可能版本:普通文件和压缩文件。这样做是为了防止只删掉一个副本,另一个副本还留在磁盘上。
数据流:输入是本地存储对象、一个 rollout 文件路径,以及线程编号。它先把路径规范成普通 rollout 文件路径,再根据这个路径推算出压缩版路径,也就是扩展名为 jsonl.zst 的文件。然后它分别调用 delete_rollout_path 删除普通版和压缩版。最后返回一个布尔值:只要其中任意一个文件真的被删掉,就返回 true;如果两个都已经不存在,就返回 false。
调用关系:它被 delete_thread 在找到每个 rollout 文件后调用。它自己不直接判断路径安全和文件名匹配,而是把每一个具体路径交给 delete_rollout_path 去做更严格的检查和删除。
调用图:调用 1 个内部函数(delete_rollout_path);被 1 处调用(delete_thread);外部调用 1 个(plain_rollout_path)。
delete_rollout_path99–131 ↗
fn delete_rollout_path(
store: &LocalThreadStore,
rollout_path: &Path,
thread_id: codex_protocol::ThreadId,
) -> ThreadStoreResult<bool>
作用:这个函数真正执行“删除某个磁盘文件”的动作,并且在删除前做安全检查。它的重点是防止路径乱跳导致误删系统里别的文件。
数据流:输入是本地存储对象、要删的文件路径和线程编号。它先尝试把这个路径限制在 sessions 目录里;如果不在,再尝试限制在 archived sessions 目录里。这里的“限制”可以理解成确认文件确实在系统允许的抽屉里,而不是用户随便给了一个危险路径。如果文件已经不存在,它允许继续往下走,把它当作已经删掉。接着它检查文件名是否和线程编号匹配。最后调用系统的 remove_file 删除文件:删成功返回 true;文件已不存在返回 false;其他磁盘错误会包装成内部错误返回。
调用关系:它是 delete_rollout_file 的底层执行者。上层负责决定要删普通版还是压缩版,它负责确认路径安全、名字正确,并执行真正的文件删除。它会用 scoped_rollout_path 做目录范围检查,用 matching_rollout_file_name 做文件名和线程编号的核对。
调用图:调用 2 个内部函数(matching_rollout_file_name, scoped_rollout_path);被 1 处调用(delete_rollout_file);外部调用 2 个(format!, remove_file)。
tests::delete_thread_removes_active_and_archived_rollouts148–179 ↗
async fn delete_thread_removes_active_and_archived_rollouts()
作用:这个测试确认删除线程时,普通会话文件、归档会话文件,以及压缩副本都会被删掉。它保证主流程不会只顾一种目录或一种文件格式。
数据流:测试先创建一个临时目录和本地存储对象,再写入一个普通会话文件,并额外写一个对应的压缩文件。随后它又写入一个归档会话文件。对每个准备好的线程编号,它调用 store.delete_thread。调用结束后,它检查原文件路径已经不存在,并额外检查压缩副本也不存在。
调用关系:这是对 delete_thread 主流程的覆盖测试。它通过 test_config、write_session_file 和 write_archived_session_file 搭出真实文件场景,然后调用公开的删除接口,间接验证 delete_thread、delete_rollout_file 和 delete_rollout_path 能配合完成删除。
调用图:调用 5 个内部函数(from_string, new, test_config, write_archived_session_file, write_session_file);外部调用 4 个(new, from_u128, assert!, write)。
tests::delete_rollout_file_treats_vanished_path_as_already_deleted182–192 ↗
async fn delete_rollout_file_treats_vanished_path_as_already_deleted()
作用:这个测试确认一种边界情况:文件刚被发现后又消失了,删除逻辑应该把它当成已经删掉,而不是报错。这样可以避免并发删除或临时文件变化造成误报警。
数据流:测试先创建临时目录和本地存储对象,写入一个会话文件,然后立刻用系统 remove_file 把它删掉。接着它直接调用 delete_rollout_file。因为目标文件已经不在了,函数应该成功返回 false,表示没有新删掉文件,但这不是错误。
调用关系:它直接测试 delete_rollout_file,并间接覆盖 delete_rollout_path 对“文件不存在”的处理。这个测试对应文件顶部注释里的规则:发现后消失的 rollout 文件算作已经删除。
调用图:调用 4 个内部函数(from_string, new, test_config, write_session_file);外部调用 4 个(new, from_u128, assert!, remove_file)。
tests::delete_thread_reports_missing_thread195–209 ↗
async fn delete_thread_reports_missing_thread()
作用:这个测试确认删除一个根本不存在的线程时,系统会明确返回“线程不存在”。这能防止调用方误以为删除成功了。
数据流:测试创建一个空的临时本地存储,然后构造一个没有对应文件的线程编号。它调用 store.delete_thread,并期待得到错误。最后它检查错误文字正好是这个线程不存在。
调用关系:它测试 delete_thread 在没有找到普通会话文件、也没有找到归档会话文件时的失败路径。它保证上层调用者能得到清楚的 ThreadNotFound 错误,而不是模糊的内部错误或假成功。
调用图:调用 3 个内部函数(from_string, new, test_config);外部调用 2 个(new, assert_eq!)。