Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,14 @@ Zcode 继续使用来源 `session.title`。私有索引以可选字段保留来
Codex 恢复或重试可能留下多份相同线程 ID 的 rollout。目录在分页和计数前按线程去重,
优先使用 `threads.rollout_path`,旧数据库缺少该字段时按来源记录时间选择;旧文件候选 ID
在读取正文时也解析到当前记录,避免导入已中断的早期副本。不会拼接已放弃的重试分支。
Codex 的 `threads.history_mode = paginated` 会话直接从同一状态快照发现,不要求旧 rollout
文件存在。正文在打开时以只读事务读取 `thread_history_1.sqlite` 的 `thread_turns` 和
`thread_items`,包含已提交 WAL,按来源序号保留用户输入、助手输出、思考和非执行工具历史。
同一线程同时存在 SQLite 与 JSONL 时使用分页历史;原有文件候选 ID 仍能通过状态库定位,
来源线程 ID 不变。分页来源中的显式 subagent 记录不作为独立主会话;源应用归档仍不覆盖本地选择。
目录读取不访问历史正文,旧版缺少分页字段的数据库及 JSONL 继续可读。不可读、未知格式、孤立条目
和超预算正文明确失败,不回退到残留 JSONL 或发布空历史;分页压缩标记没有替换上下文时要求完整历史。
该格式兼容只在本地 adapter 和实际只读来源上验证,不增加远程工作区、远程控制、Peer 或 Detached Dispatch 入口。
对旧 `codex-rollout-jsonl-v1` 中只有用户输入、没有助手文本或工具历史的副本,显式读取时
检查全部原有轮次仍带来源标记、用户输入与新来源前缀相同且没有本地新增内容,再补齐历史。
修复保留原生名称、归属及用户元数据;存储服务先保留原副本和目标副本的恢复记录,再逐个
Expand Down
5 changes: 4 additions & 1 deletion src/crates/adapters/codex-adapter/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@
## Session discovery and import verification

```bash
cargo test -p openbitfun-codex-adapter --lib session::tests
cargo test -p openbitfun-codex-adapter --lib session::
# Optional read-only installed-source verification (prints counts only):
cargo test -p openbitfun-codex-adapter --lib inspect_installed_codex_session_titles -- --ignored --nocapture
# Set OPENBITFUN_TEST_CODEX_THREAD to inspect one logical thread's full history:
cargo test -p openbitfun-codex-adapter --lib inspect_installed_codex_thread_history -- --ignored --nocapture
# Verify all paginated source IDs and histories. Optionally set
# OPENBITFUN_TEST_CODEX_WORKSPACE to restrict verification to one workspace:
cargo test -p openbitfun-codex-adapter --lib inspect_installed_codex_paginated_workspace -- --ignored --nocapture
```

## Pet source verification
Expand Down
61 changes: 49 additions & 12 deletions src/crates/adapters/codex-adapter/src/session.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! Bounded, read-only Codex rollout import. Discovery reads headers, never full histories.
//! Bounded, read-only Codex rollout and paginated SQLite history import.
//! Discovery reads metadata, never full histories.
use openbitfun_product_domains::session_import::*;
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
Expand All @@ -9,10 +10,13 @@ use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};

mod sqlite_history;
#[cfg(test)]
mod sqlite_history_tests;
mod titles;
use titles::{TitleCache, TitleSnapshot};

pub const SOURCE_REVISION: &str = "codex-rollout-jsonl-v2";
pub const SOURCE_REVISION: &str = "codex-rollout-and-sqlite-v3";
const MAX_FILE_BYTES: u64 = 256 * 1024 * 1024;
const MAX_LINE_BYTES: usize = 8 * 1024 * 1024;
const MAX_DOCUMENT_BYTES: usize = 128 * 1024 * 1024;
Expand Down Expand Up @@ -108,20 +112,39 @@ impl CodexSessionReader {
let titles = self.title_snapshot();
let mut diagnostics = titles.diagnostics.clone();
let paths = self.logical_session_paths(&titles, &mut diagnostics)?;
let mut available = titles
.paginated
.values()
.filter(|thread| !thread.subagent)
.map(|thread| {
let mut candidate = thread.candidate.clone();
titles.apply(&mut candidate);
candidate
})
.collect::<Vec<_>>();
for path in paths {
match self.inspect_path_with_titles(&path, &titles) {
Ok(candidate) if !titles.paginated.contains_key(&candidate.source_session_id) => {
available.push(candidate)
}
Ok(_) => {}
Err(error) if diagnostics.len() < 20 => {
diagnostics.push(warning("source_unreadable", &error))
}
Err(_) => {}
}
}
available.sort_by(|left, right| {
right
.updated_at
.cmp(&left.updated_at)
.then_with(|| left.candidate_id.cmp(&right.candidate_id))
});
let mut candidates = Vec::new();
let mut matched = 0usize;
let mut has_more = false;
let query = query.to_lowercase();
for path in paths {
let candidate = match self.inspect_path_with_titles(&path, &titles) {
Ok(candidate) => candidate,
Err(error) => {
if diagnostics.len() < 20 {
diagnostics.push(warning("source_unreadable", &error));
}
continue;
}
};
for candidate in available {
if !query.is_empty()
&& !format!(
"{} {}",
Expand Down Expand Up @@ -158,6 +181,17 @@ impl CodexSessionReader {
id: &str,
mode: ExternalSessionImportMode,
) -> Result<CodexSessionDocument, String> {
if !self.codex_home.is_absolute() {
return Err("Codex home must be an absolute host path".into());
}
let titles = self.title_snapshot();
if let Some(thread) = titles
.paginated
.values()
.find(|thread| thread.matches_locator(id))
{
return sqlite_history::read(thread, mode, &titles);
}
let cached = self
.headers
.lock()
Expand Down Expand Up @@ -233,6 +267,9 @@ impl CodexSessionReader {
) -> Result<CodexSessionDocument, String> {
let candidate = self.inspect_header(path)?;
let titles = self.title_snapshot();
if let Some(thread) = titles.paginated.get(&candidate.source_session_id) {
return sqlite_history::read(thread, mode, &titles);
}
// A resume can publish a new rollout after the last catalog scan. Read
// only this thread's indexed file, after checking the source boundary.
if let Some(current) = titles.current_rollout(&candidate.source_session_id) {
Expand Down
Loading
Loading