M0: compilable skeleton — Kigi 0.1.0 fork surgery
Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.
Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
ptyctl, ptyctl-cli, third_party/ unchanged; proto package
xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
(templates re-encrypted)
Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
module & dc_log, heap-profile uploader, auth-diagnostics uploader,
session-analytics halves of feedback; local zero-egress observability
preserved in new kigi-log crate (unified log, --debug firehose,
subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
shell util
Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted
Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean
Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
(new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
fast-worktree); RSS measurement tests serialized via serial_test
Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
notices sustained; kigi-tools ported-code notices extended; README,
CONTRIBUTING, SECURITY, AGENTS.md rewritten
Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
This commit is contained in:
commit
d6c20fc13f
2612 files changed
+1353757
No files matched your search
@@ -0,0 +1,291 @@
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use rusqlite::{Connection, params};
|
||||
|
||||
use super::super::{
|
||||
ApprovedRoot, ForeignSessionSummary, ForeignSessionTool, MAX_SESSION_AGE,
|
||||
MAX_SESSIONS_PER_TOOL, RecentCandidate, is_within, millis_bounds, normalize_title,
|
||||
open_sqlite_transaction, system_time_from_millis,
|
||||
};
|
||||
use super::{existing_rollout_path, source_from_persisted, title};
|
||||
|
||||
const MAX_DB_CANDIDATES: usize = 200;
|
||||
pub(super) const MAX_RECENT_DB_CANDIDATES: usize = 8;
|
||||
const MIN_EPOCH_MILLIS: i64 = 1_577_836_800_000;
|
||||
const MAX_ID_BYTES: usize = 64;
|
||||
const MAX_PATH_BYTES: usize = 16 * 1024;
|
||||
const MAX_TEXT_BYTES: usize = 64 * 1024;
|
||||
const MAX_BRANCH_BYTES: usize = 4 * 1024;
|
||||
|
||||
struct DbCandidate {
|
||||
id: String,
|
||||
rollout_path: String,
|
||||
updated_at: i64,
|
||||
source: String,
|
||||
stored_cwd: String,
|
||||
title: String,
|
||||
first_user_message: String,
|
||||
branch: Option<String>,
|
||||
}
|
||||
|
||||
pub(super) enum RecentDatabaseResult {
|
||||
Unusable,
|
||||
Incomplete,
|
||||
Usable(Option<RecentCandidate>),
|
||||
}
|
||||
|
||||
pub(super) fn scan_database(
|
||||
root: &ApprovedRoot,
|
||||
db_path: &Path,
|
||||
cwd: &Path,
|
||||
now: SystemTime,
|
||||
) -> Option<Vec<ForeignSessionSummary>> {
|
||||
let bounds = millis_bounds(now, MAX_SESSION_AGE)?;
|
||||
let cwd_string = cwd.to_str()?.to_owned();
|
||||
if cwd_string.len() > MAX_PATH_BYTES {
|
||||
return None;
|
||||
}
|
||||
let database = open_sqlite_transaction(root, db_path)?;
|
||||
let columns = table_columns(&database, "threads")?;
|
||||
let sql = scan_sql(&columns)?;
|
||||
let rows = query_candidates(&database, &sql, &cwd_string, bounds)?;
|
||||
let mut sessions = Vec::new();
|
||||
for row in rows {
|
||||
let Some(candidate) =
|
||||
qualify_candidate(root, cwd, now, MAX_SESSION_AGE, &row, source_from_persisted)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(title) = title(&row.title, &row.first_user_message) else {
|
||||
continue;
|
||||
};
|
||||
sessions.push(ForeignSessionSummary {
|
||||
tool: ForeignSessionTool::Codex,
|
||||
source: candidate.source,
|
||||
native_id: candidate.native_id,
|
||||
title,
|
||||
cwd: Path::new(&row.stored_cwd).to_path_buf(),
|
||||
updated_at: candidate.updated_at,
|
||||
branch: row.branch.as_deref().and_then(normalize_title),
|
||||
});
|
||||
if sessions.len() == MAX_SESSIONS_PER_TOOL {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(sessions)
|
||||
}
|
||||
|
||||
pub(super) fn most_recent_database(
|
||||
root: &ApprovedRoot,
|
||||
db_path: &Path,
|
||||
cwd: &Path,
|
||||
now: SystemTime,
|
||||
within: Duration,
|
||||
) -> RecentDatabaseResult {
|
||||
let Some(bounds) = millis_bounds(now, within) else {
|
||||
return RecentDatabaseResult::Unusable;
|
||||
};
|
||||
let Some(cwd_string) = cwd.to_str().map(str::to_owned) else {
|
||||
return RecentDatabaseResult::Unusable;
|
||||
};
|
||||
if cwd_string.len() > MAX_PATH_BYTES {
|
||||
return RecentDatabaseResult::Unusable;
|
||||
}
|
||||
let Some(database) = open_sqlite_transaction(root, db_path) else {
|
||||
return RecentDatabaseResult::Unusable;
|
||||
};
|
||||
let Some(columns) = table_columns(&database, "threads") else {
|
||||
return RecentDatabaseResult::Unusable;
|
||||
};
|
||||
let Some(sql) = recent_scan_sql(&columns) else {
|
||||
return RecentDatabaseResult::Unusable;
|
||||
};
|
||||
let Ok(mut rows) = query_recent_candidates(&database, &sql, &cwd_string, bounds) else {
|
||||
return RecentDatabaseResult::Incomplete;
|
||||
};
|
||||
let truncated = rows.len() > MAX_RECENT_DB_CANDIDATES;
|
||||
rows.truncate(MAX_RECENT_DB_CANDIDATES);
|
||||
let candidate = rows
|
||||
.into_iter()
|
||||
.find_map(|row| qualify_candidate(root, cwd, now, within, &row, super::source_from_str));
|
||||
if candidate.is_none() && truncated {
|
||||
RecentDatabaseResult::Incomplete
|
||||
} else {
|
||||
RecentDatabaseResult::Usable(candidate)
|
||||
}
|
||||
}
|
||||
|
||||
fn query_candidates(
|
||||
database: &Connection,
|
||||
sql: &str,
|
||||
cwd: &str,
|
||||
(oldest_millis, newest_millis): (i64, i64),
|
||||
) -> Option<Vec<DbCandidate>> {
|
||||
let mut statement = database.prepare(sql).ok()?;
|
||||
let rows = statement
|
||||
.query_map(
|
||||
params![cwd, oldest_millis, newest_millis],
|
||||
decode_candidate_row,
|
||||
)
|
||||
.ok()?;
|
||||
Some(rows.flatten().collect())
|
||||
}
|
||||
|
||||
fn query_recent_candidates(
|
||||
database: &Connection,
|
||||
sql: &str,
|
||||
cwd: &str,
|
||||
(oldest_millis, newest_millis): (i64, i64),
|
||||
) -> Result<Vec<DbCandidate>, ()> {
|
||||
let mut statement = database.prepare(sql).map_err(|_| ())?;
|
||||
let rows = statement
|
||||
.query_map(
|
||||
params![cwd, oldest_millis, newest_millis],
|
||||
decode_candidate_row,
|
||||
)
|
||||
.map_err(|_| ())?;
|
||||
rows.collect::<rusqlite::Result<Vec<_>>>().map_err(|_| ())
|
||||
}
|
||||
|
||||
fn decode_candidate_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<DbCandidate> {
|
||||
Ok(DbCandidate {
|
||||
id: row.get(0)?,
|
||||
rollout_path: row.get(1)?,
|
||||
updated_at: row.get(2)?,
|
||||
source: row.get(3)?,
|
||||
stored_cwd: row.get(4)?,
|
||||
title: row.get(5)?,
|
||||
first_user_message: row.get(6)?,
|
||||
branch: row.get(7)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn qualify_candidate(
|
||||
root: &ApprovedRoot,
|
||||
cwd: &Path,
|
||||
now: SystemTime,
|
||||
within: Duration,
|
||||
row: &DbCandidate,
|
||||
parse_source: impl Fn(&str) -> Option<super::super::ForeignSessionSource>,
|
||||
) -> Option<RecentCandidate> {
|
||||
if uuid::Uuid::try_parse(&row.id).is_err() || Path::new(&row.stored_cwd) != cwd {
|
||||
return None;
|
||||
}
|
||||
let source = parse_source(&row.source)?;
|
||||
let updated_at = normalize_updated_at(row.updated_at)?;
|
||||
if !is_within(updated_at, now, within)
|
||||
|| existing_rollout_path(root, &row.rollout_path, &row.id).is_none()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(RecentCandidate {
|
||||
tool: ForeignSessionTool::Codex,
|
||||
source,
|
||||
native_id: row.id.clone(),
|
||||
updated_at,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn normalize_updated_at(value: i64) -> Option<SystemTime> {
|
||||
let millis = if value < MIN_EPOCH_MILLIS {
|
||||
value.saturating_mul(1_000)
|
||||
} else {
|
||||
value
|
||||
};
|
||||
system_time_from_millis(millis)
|
||||
}
|
||||
|
||||
pub(super) fn scan_sql(columns: &HashSet<String>) -> Option<String> {
|
||||
scan_sql_with_limit(
|
||||
columns,
|
||||
MAX_DB_CANDIDATES,
|
||||
"('cli', 'vscode', '{\"custom\":\"atlas\"}', '{\"custom\":\"chatgpt\"}')",
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn recent_scan_sql(columns: &HashSet<String>) -> Option<String> {
|
||||
scan_sql_with_limit(columns, MAX_RECENT_DB_CANDIDATES + 1, "('cli', 'vscode')")
|
||||
}
|
||||
|
||||
fn scan_sql_with_limit(
|
||||
columns: &HashSet<String>,
|
||||
limit: usize,
|
||||
allowed_sources: &str,
|
||||
) -> Option<String> {
|
||||
for required in ["id", "rollout_path", "source", "cwd", "archived"] {
|
||||
if !columns.contains(required) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let updated_column = if columns.contains("updated_at_ms") {
|
||||
"updated_at_ms"
|
||||
} else if columns.contains("updated_at") {
|
||||
"updated_at"
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
let title_column = if columns.contains("title") {
|
||||
"title"
|
||||
} else {
|
||||
"''"
|
||||
};
|
||||
let first_user_message = if columns.contains("first_user_message") {
|
||||
"first_user_message"
|
||||
} else {
|
||||
"''"
|
||||
};
|
||||
let git_branch = if columns.contains("git_branch") {
|
||||
"git_branch"
|
||||
} else {
|
||||
"NULL"
|
||||
};
|
||||
let title_projection = format!(
|
||||
"CASE WHEN typeof({title_column}) = 'text' \
|
||||
AND octet_length({title_column}) <= {MAX_TEXT_BYTES} \
|
||||
THEN {title_column} ELSE '' END"
|
||||
);
|
||||
let first_projection = format!(
|
||||
"CASE WHEN typeof({first_user_message}) = 'text' \
|
||||
AND octet_length({first_user_message}) <= {MAX_TEXT_BYTES} \
|
||||
THEN {first_user_message} ELSE '' END"
|
||||
);
|
||||
let branch_projection = format!(
|
||||
"CASE WHEN typeof({git_branch}) = 'text' \
|
||||
AND octet_length({git_branch}) <= {MAX_BRANCH_BYTES} \
|
||||
THEN {git_branch} ELSE NULL END"
|
||||
);
|
||||
Some(format!(
|
||||
"SELECT id, rollout_path, {updated_column}, source, cwd, \
|
||||
{title_projection}, {first_projection}, {branch_projection} \
|
||||
FROM threads \
|
||||
WHERE typeof(id) = 'text' \
|
||||
AND typeof(rollout_path) = 'text' \
|
||||
AND typeof({updated_column}) = 'integer' \
|
||||
AND typeof(archived) = 'integer' \
|
||||
AND archived = 0 AND cwd = ?1 \
|
||||
AND source IN {allowed_sources} \
|
||||
AND octet_length(id) <= {MAX_ID_BYTES} \
|
||||
AND octet_length(rollout_path) <= {MAX_PATH_BYTES} \
|
||||
AND CASE \
|
||||
WHEN {updated_column} < {MIN_EPOCH_MILLIS} THEN {updated_column} * 1000 \
|
||||
ELSE {updated_column} \
|
||||
END BETWEEN ?2 AND ?3 \
|
||||
ORDER BY CASE \
|
||||
WHEN {updated_column} < {MIN_EPOCH_MILLIS} THEN {updated_column} * 1000 \
|
||||
ELSE {updated_column} \
|
||||
END DESC, id ASC LIMIT {limit}"
|
||||
))
|
||||
}
|
||||
|
||||
fn table_columns(connection: &Connection, table: &str) -> Option<HashSet<String>> {
|
||||
let mut statement = connection
|
||||
.prepare(&format!("PRAGMA table_info({table})"))
|
||||
.ok()?;
|
||||
let rows = statement
|
||||
.query_map([], |row| row.get::<_, String>(1))
|
||||
.ok()?;
|
||||
let columns = rows.flatten().collect::<HashSet<_>>();
|
||||
(!columns.is_empty()).then_some(columns)
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
use std::collections::HashSet;
|
||||
use std::io::{BufRead, BufReader, Read};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use chrono::{DateTime, Datelike, Days, Local, TimeDelta, Utc};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::super::{
|
||||
ApprovedRoot, ForeignSessionSource, ForeignSessionSummary, ForeignSessionTool, MAX_SESSION_AGE,
|
||||
MAX_SESSIONS_PER_TOOL, RecentCandidate, RecentProbe, is_within, normalize_title,
|
||||
retain_top_k_by,
|
||||
};
|
||||
use super::{rollout_id, source_from_value, title};
|
||||
|
||||
const DAYS_IN_WINDOW: usize = 31;
|
||||
const MAX_DATE_DIRS: usize = 32;
|
||||
const MAX_METADATA_READS: usize = 128;
|
||||
const MAX_RECENT_METADATA_READS: usize = 16;
|
||||
pub(super) const MAX_RECENT_DIRECTORY_ENTRIES: usize = 64;
|
||||
const MAX_HEAD_RECORDS: usize = 10;
|
||||
pub(super) const MAX_HEAD_BYTES: usize = 64 * 1024;
|
||||
// Bound compressed work separately from the decoded head and decoder window.
|
||||
pub(super) const MAX_COMPRESSED_HEAD_BYTES: usize = 256 * 1024;
|
||||
pub(super) const MAX_ZSTD_WINDOW_LOG: u32 = 23;
|
||||
|
||||
struct Candidate {
|
||||
root: ApprovedRoot,
|
||||
path: PathBuf,
|
||||
id: String,
|
||||
modified: SystemTime,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct HeadMetadata {
|
||||
id: Option<uuid::Uuid>,
|
||||
cwd: Option<String>,
|
||||
source: Option<ForeignSessionSource>,
|
||||
branch: Option<String>,
|
||||
first_user_message: Option<String>,
|
||||
}
|
||||
|
||||
pub(super) fn scan_rollouts(
|
||||
codex_root: &ApprovedRoot,
|
||||
cwd: &Path,
|
||||
now: SystemTime,
|
||||
) -> Vec<ForeignSessionSummary> {
|
||||
let Some(root) = codex_root.subroot(Path::new("sessions")) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let candidates = collect_candidates(&root, now, MAX_SESSION_AGE, MAX_METADATA_READS);
|
||||
let mut accepted_ids = HashSet::new();
|
||||
let mut sessions = Vec::new();
|
||||
for candidate in candidates {
|
||||
if accepted_ids.contains(&candidate.id) {
|
||||
continue;
|
||||
}
|
||||
let Some(session) = read_candidate(candidate, cwd) else {
|
||||
continue;
|
||||
};
|
||||
accepted_ids.insert(session.native_id.clone());
|
||||
sessions.push(session);
|
||||
if sessions.len() == MAX_SESSIONS_PER_TOOL {
|
||||
break;
|
||||
}
|
||||
}
|
||||
sessions
|
||||
}
|
||||
|
||||
pub(super) fn most_recent_rollout(
|
||||
codex_root: &ApprovedRoot,
|
||||
cwd: &Path,
|
||||
now: SystemTime,
|
||||
within: Duration,
|
||||
) -> RecentProbe<RecentCandidate> {
|
||||
let sessions_path = codex_root.join("sessions");
|
||||
match std::fs::symlink_metadata(&sessions_path) {
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
||||
return RecentProbe::Complete(None);
|
||||
}
|
||||
Err(_) => return RecentProbe::Incomplete,
|
||||
Ok(_) => {}
|
||||
}
|
||||
let Some(root) = codex_root.subroot(&sessions_path) else {
|
||||
return RecentProbe::Incomplete;
|
||||
};
|
||||
let Some((candidates, truncated)) = collect_recent_candidates(&root, now, within) else {
|
||||
return RecentProbe::Incomplete;
|
||||
};
|
||||
let candidate = candidates.into_iter().find_map(|candidate| {
|
||||
let (_, source, _) = qualify_candidate(&candidate, cwd)?;
|
||||
if !matches!(
|
||||
source,
|
||||
ForeignSessionSource::CodexCli | ForeignSessionSource::CodexVsCode
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
Some(RecentCandidate {
|
||||
tool: ForeignSessionTool::Codex,
|
||||
source,
|
||||
native_id: candidate.id,
|
||||
updated_at: candidate.modified,
|
||||
})
|
||||
});
|
||||
if candidate.is_none() && truncated {
|
||||
RecentProbe::Incomplete
|
||||
} else {
|
||||
RecentProbe::Complete(candidate)
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_recent_candidates(
|
||||
root: &ApprovedRoot,
|
||||
now: SystemTime,
|
||||
within: Duration,
|
||||
) -> Option<(Vec<Candidate>, bool)> {
|
||||
let mut candidates = Vec::with_capacity(MAX_RECENT_METADATA_READS);
|
||||
let mut remaining_entries = MAX_RECENT_DIRECTORY_ENTRIES;
|
||||
let mut qualifying = 0;
|
||||
let local_offset = DateTime::<Local>::from(now).offset().local_minus_utc();
|
||||
for date_dir in recent_date_dirs(root.path(), now, local_offset) {
|
||||
let Some(date_root) = root.subroot(&date_dir) else {
|
||||
continue;
|
||||
};
|
||||
let outcome = date_root.for_each_entry_bounded(remaining_entries, |name| {
|
||||
let path = date_root.join(&name);
|
||||
let Some(id) = rollout_id(&path) else {
|
||||
return;
|
||||
};
|
||||
let Some((path, metadata)) = date_root.resolve_regular_file(&path) else {
|
||||
return;
|
||||
};
|
||||
let Ok(modified) = metadata.modified() else {
|
||||
return;
|
||||
};
|
||||
if is_within(modified, now, within) {
|
||||
qualifying += 1;
|
||||
retain_top_k_by(
|
||||
&mut candidates,
|
||||
Candidate {
|
||||
root: date_root.clone(),
|
||||
path,
|
||||
id,
|
||||
modified,
|
||||
},
|
||||
MAX_RECENT_METADATA_READS,
|
||||
candidate_order,
|
||||
);
|
||||
}
|
||||
});
|
||||
remaining_entries = remaining_entries.saturating_sub(outcome.visited);
|
||||
if !outcome.complete {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some((candidates, qualifying > MAX_RECENT_METADATA_READS))
|
||||
}
|
||||
|
||||
fn collect_candidates(
|
||||
root: &ApprovedRoot,
|
||||
now: SystemTime,
|
||||
within: Duration,
|
||||
limit: usize,
|
||||
) -> Vec<Candidate> {
|
||||
let mut candidates = Vec::with_capacity(limit);
|
||||
let local_offset = DateTime::<Local>::from(now).offset().local_minus_utc();
|
||||
for date_dir in recent_date_dirs(root.path(), now, local_offset) {
|
||||
let Some(date_root) = root.subroot(&date_dir) else {
|
||||
continue;
|
||||
};
|
||||
let mut date_candidates = Vec::with_capacity(limit);
|
||||
// Enumerate every direct rollout entry in the fixed date window so
|
||||
// filesystem order cannot decide which files receive the head-read budget.
|
||||
let complete = date_root.for_each_entry(|name| {
|
||||
let path = date_root.join(&name);
|
||||
let Some(id) = rollout_id(&path) else {
|
||||
return;
|
||||
};
|
||||
let Some((path, metadata)) = date_root.resolve_regular_file(&path) else {
|
||||
return;
|
||||
};
|
||||
let Ok(modified) = metadata.modified() else {
|
||||
return;
|
||||
};
|
||||
if is_within(modified, now, within) {
|
||||
retain_top_k_by(
|
||||
&mut date_candidates,
|
||||
Candidate {
|
||||
root: date_root.clone(),
|
||||
path,
|
||||
id,
|
||||
modified,
|
||||
},
|
||||
limit,
|
||||
candidate_order,
|
||||
);
|
||||
}
|
||||
});
|
||||
if !complete {
|
||||
continue;
|
||||
}
|
||||
for candidate in date_candidates {
|
||||
retain_top_k_by(&mut candidates, candidate, limit, candidate_order);
|
||||
}
|
||||
}
|
||||
candidates
|
||||
}
|
||||
|
||||
fn candidate_order(a: &Candidate, b: &Candidate) -> std::cmp::Ordering {
|
||||
b.modified
|
||||
.cmp(&a.modified)
|
||||
.then_with(|| a.id.cmp(&b.id))
|
||||
.then_with(|| a.path.cmp(&b.path))
|
||||
}
|
||||
|
||||
pub(super) fn recent_date_dirs(
|
||||
root: &Path,
|
||||
now: SystemTime,
|
||||
local_offset_seconds: i32,
|
||||
) -> Vec<PathBuf> {
|
||||
let utc = DateTime::<Utc>::from(now);
|
||||
let local = utc + TimeDelta::seconds(i64::from(local_offset_seconds));
|
||||
let mut dates = [utc.date_naive(), local.date_naive()]
|
||||
.into_iter()
|
||||
.flat_map(|today| {
|
||||
(0..DAYS_IN_WINDOW)
|
||||
.filter_map(move |days| today.checked_sub_days(Days::new(days as u64)))
|
||||
})
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
dates.sort_by(|a, b| b.cmp(a));
|
||||
dates.truncate(MAX_DATE_DIRS);
|
||||
dates
|
||||
.into_iter()
|
||||
.map(|date| {
|
||||
root.join(format!("{:04}", date.year()))
|
||||
.join(format!("{:02}", date.month()))
|
||||
.join(format!("{:02}", date.day()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read_candidate(candidate: Candidate, requested_cwd: &Path) -> Option<ForeignSessionSummary> {
|
||||
let (
|
||||
HeadMetadata {
|
||||
branch,
|
||||
first_user_message,
|
||||
..
|
||||
},
|
||||
source,
|
||||
stored_cwd,
|
||||
) = qualify_candidate(&candidate, requested_cwd)?;
|
||||
let title = title("", first_user_message.as_deref().unwrap_or(""))?;
|
||||
Some(ForeignSessionSummary {
|
||||
tool: ForeignSessionTool::Codex,
|
||||
source,
|
||||
native_id: candidate.id,
|
||||
title,
|
||||
cwd: PathBuf::from(stored_cwd),
|
||||
updated_at: candidate.modified,
|
||||
branch,
|
||||
})
|
||||
}
|
||||
|
||||
fn qualify_candidate(
|
||||
candidate: &Candidate,
|
||||
requested_cwd: &Path,
|
||||
) -> Option<(HeadMetadata, ForeignSessionSource, String)> {
|
||||
let metadata = read_head(&candidate.root, &candidate.path)?;
|
||||
if metadata.id? != uuid::Uuid::try_parse(&candidate.id).ok()? {
|
||||
return None;
|
||||
}
|
||||
let stored_cwd = metadata.cwd.clone()?;
|
||||
if Path::new(&stored_cwd) != requested_cwd {
|
||||
return None;
|
||||
}
|
||||
let source = metadata.source?;
|
||||
Some((metadata, source, stored_cwd))
|
||||
}
|
||||
|
||||
fn read_head(root: &ApprovedRoot, path: &Path) -> Option<HeadMetadata> {
|
||||
let file = root.open_regular_file(path)?.file;
|
||||
if path.extension().and_then(|extension| extension.to_str()) == Some("zst") {
|
||||
let compressed = file.take(MAX_COMPRESSED_HEAD_BYTES as u64);
|
||||
let mut decoder = zstd::Decoder::new(compressed).ok()?;
|
||||
decoder.window_log_max(MAX_ZSTD_WINDOW_LOG).ok()?;
|
||||
let decoder = decoder.single_frame();
|
||||
parse_head(BufReader::new(decoder.take(MAX_HEAD_BYTES as u64)))
|
||||
} else {
|
||||
parse_head(BufReader::new(file.take(MAX_HEAD_BYTES as u64)))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_head(mut reader: impl BufRead) -> Option<HeadMetadata> {
|
||||
let mut line = String::new();
|
||||
let mut total = 0;
|
||||
let mut metadata = HeadMetadata::default();
|
||||
let mut saw_session_meta = false;
|
||||
for _ in 0..MAX_HEAD_RECORDS {
|
||||
line.clear();
|
||||
let read = reader.read_line(&mut line).ok()?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
total += read;
|
||||
if total > MAX_HEAD_BYTES {
|
||||
break;
|
||||
}
|
||||
let Ok(record) = serde_json::from_str::<Value>(&line) else {
|
||||
continue;
|
||||
};
|
||||
let record_type = record.get("type").and_then(Value::as_str);
|
||||
let payload = record.get("payload").unwrap_or(&Value::Null);
|
||||
if record_type == Some("session_meta") && !saw_session_meta {
|
||||
saw_session_meta = true;
|
||||
metadata.id = payload
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.and_then(|id| uuid::Uuid::try_parse(id).ok());
|
||||
metadata.cwd = payload
|
||||
.get("cwd")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_owned);
|
||||
metadata.source = payload.get("source").and_then(source_from_value);
|
||||
metadata.branch = payload
|
||||
.pointer("/git/branch")
|
||||
.or_else(|| payload.get("git_branch"))
|
||||
.and_then(Value::as_str)
|
||||
.and_then(normalize_title);
|
||||
}
|
||||
if metadata.first_user_message.is_none() {
|
||||
metadata.first_user_message = user_message(payload).and_then(normalize_title);
|
||||
}
|
||||
}
|
||||
Some(metadata)
|
||||
}
|
||||
|
||||
fn user_message(payload: &Value) -> Option<&str> {
|
||||
if payload.get("type").and_then(Value::as_str) == Some("user_message") {
|
||||
return payload.get("message").and_then(Value::as_str);
|
||||
}
|
||||
if payload.get("type").and_then(Value::as_str) != Some("message")
|
||||
|| payload.get("role").and_then(Value::as_str) != Some("user")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let text = payload
|
||||
.get("content")?
|
||||
.as_array()?
|
||||
.iter()
|
||||
.find_map(|item| match item.get("type").and_then(Value::as_str) {
|
||||
Some("input_text" | "text") => item.get("text").and_then(Value::as_str),
|
||||
_ => None,
|
||||
})?;
|
||||
let trimmed = text.trim_start();
|
||||
(!trimmed.starts_with("<environment_context>") && !trimmed.starts_with("<user_instructions>"))
|
||||
.then_some(text)
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
mod db;
|
||||
mod files;
|
||||
|
||||
use super::{
|
||||
ApprovedRoot, ForeignSessionSource, ForeignSessionSummary, RecentCandidate, RecentProbe,
|
||||
approved_root_for_recent, finish_tool_scan, normalize_title,
|
||||
};
|
||||
|
||||
// Codex is currently on single-digit state generations. Probe a generous,
|
||||
// deterministic supported range without enumerating unrelated CODEX_HOME files.
|
||||
const MAX_STATE_DB_GENERATION: u32 = 128;
|
||||
|
||||
pub(super) fn scan(cwd: &Path, now: SystemTime) -> Vec<ForeignSessionSummary> {
|
||||
let Some(codex_home) = std::env::var_os("CODEX_HOME")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| dirs::home_dir().map(|home| home.join(".codex")))
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
scan_in_home(&codex_home, cwd, now)
|
||||
}
|
||||
|
||||
pub(super) fn most_recent(
|
||||
cwd: &Path,
|
||||
now: SystemTime,
|
||||
within: Duration,
|
||||
) -> RecentProbe<RecentCandidate> {
|
||||
let Some(codex_home) = std::env::var_os("CODEX_HOME")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| dirs::home_dir().map(|home| home.join(".codex")))
|
||||
else {
|
||||
return RecentProbe::Complete(None);
|
||||
};
|
||||
most_recent_in_home(&codex_home, cwd, now, within)
|
||||
}
|
||||
|
||||
fn scan_in_home(codex_home: &Path, cwd: &Path, now: SystemTime) -> Vec<ForeignSessionSummary> {
|
||||
let Some(root) = ApprovedRoot::new(codex_home) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let sessions = state_databases(&root)
|
||||
.find_map(|path| {
|
||||
db::scan_database(&root, &path, cwd, now).filter(|sessions| !sessions.is_empty())
|
||||
})
|
||||
.unwrap_or_else(|| files::scan_rollouts(&root, cwd, now));
|
||||
finish_tool_scan(sessions)
|
||||
}
|
||||
|
||||
fn most_recent_in_home(
|
||||
codex_home: &Path,
|
||||
cwd: &Path,
|
||||
now: SystemTime,
|
||||
within: Duration,
|
||||
) -> RecentProbe<RecentCandidate> {
|
||||
let root = match approved_root_for_recent(codex_home) {
|
||||
Ok(Some(root)) => root,
|
||||
Ok(None) => return RecentProbe::Complete(None),
|
||||
Err(()) => return RecentProbe::Incomplete,
|
||||
};
|
||||
let Some(path) = highest_named_state_database(&root) else {
|
||||
return files::most_recent_rollout(&root, cwd, now, within);
|
||||
};
|
||||
match db::most_recent_database(&root, &path, cwd, now, within) {
|
||||
db::RecentDatabaseResult::Usable(candidate) => RecentProbe::Complete(candidate),
|
||||
db::RecentDatabaseResult::Incomplete => RecentProbe::Incomplete,
|
||||
db::RecentDatabaseResult::Unusable => files::most_recent_rollout(&root, cwd, now, within),
|
||||
}
|
||||
}
|
||||
|
||||
fn highest_named_state_database(root: &ApprovedRoot) -> Option<PathBuf> {
|
||||
(0..=MAX_STATE_DB_GENERATION).rev().find_map(|generation| {
|
||||
let path = root.join(format!("state_{generation}.sqlite"));
|
||||
match std::fs::symlink_metadata(&path) {
|
||||
Ok(_) => Some(path),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
|
||||
Err(_) => Some(path),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn state_databases(root: &ApprovedRoot) -> impl Iterator<Item = PathBuf> + '_ {
|
||||
(0..=MAX_STATE_DB_GENERATION)
|
||||
.rev()
|
||||
.filter_map(move |generation| {
|
||||
let path = root.join(format!("state_{generation}.sqlite"));
|
||||
root.resolve_regular_file(&path).map(|(path, _)| path)
|
||||
})
|
||||
}
|
||||
|
||||
fn source_from_str(source: &str) -> Option<ForeignSessionSource> {
|
||||
match source {
|
||||
"cli" => Some(ForeignSessionSource::CodexCli),
|
||||
"vscode" => Some(ForeignSessionSource::CodexVsCode),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn source_from_persisted(source: &str) -> Option<ForeignSessionSource> {
|
||||
source_from_str(source).or_else(|| {
|
||||
let value = serde_json::from_str::<Value>(source).ok()?;
|
||||
source_from_value(&value)
|
||||
})
|
||||
}
|
||||
|
||||
fn source_from_value(source: &Value) -> Option<ForeignSessionSource> {
|
||||
source
|
||||
.as_str()
|
||||
.and_then(source_from_str)
|
||||
.or_else(|| match source.get("custom")?.as_str()? {
|
||||
"atlas" => Some(ForeignSessionSource::CodexAtlas),
|
||||
"chatgpt" => Some(ForeignSessionSource::CodexChatGpt),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn existing_rollout_path(root: &ApprovedRoot, value: &str, expected_id: &str) -> Option<PathBuf> {
|
||||
let path = PathBuf::from(value);
|
||||
if path.components().any(|part| part == Component::ParentDir) {
|
||||
return None;
|
||||
}
|
||||
let path = if path.is_absolute() {
|
||||
path
|
||||
} else {
|
||||
root.join(path)
|
||||
};
|
||||
let mut compressed = path.clone().into_os_string();
|
||||
compressed.push(".zst");
|
||||
let approved_prefixes = ["sessions", "archived_sessions"]
|
||||
.into_iter()
|
||||
.filter_map(|name| dunce::canonicalize(root.join(name)).ok())
|
||||
.filter(|path| path.starts_with(root.path()))
|
||||
.collect::<Vec<_>>();
|
||||
[path, PathBuf::from(compressed)]
|
||||
.into_iter()
|
||||
.filter_map(|candidate| root.open_regular_file(&candidate).map(|opened| opened.path))
|
||||
.find(|candidate| {
|
||||
rollout_id(candidate).as_deref() == Some(expected_id)
|
||||
&& approved_prefixes
|
||||
.iter()
|
||||
.any(|prefix| candidate.starts_with(prefix))
|
||||
})
|
||||
}
|
||||
|
||||
fn rollout_id(path: &Path) -> Option<String> {
|
||||
let name = path.file_name()?.to_str()?;
|
||||
let stem = name
|
||||
.strip_suffix(".jsonl.zst")
|
||||
.or_else(|| name.strip_suffix(".jsonl"))?;
|
||||
let value = stem.strip_prefix("rollout-")?;
|
||||
let id_start = value.len().checked_sub(36)?;
|
||||
if id_start == 0 || value.as_bytes().get(id_start - 1) != Some(&b'-') {
|
||||
return None;
|
||||
}
|
||||
let timestamp = &value[..id_start - 1];
|
||||
if timestamp.len() != 19 {
|
||||
return None;
|
||||
}
|
||||
chrono::NaiveDateTime::parse_from_str(timestamp, "%Y-%m-%dT%H-%M-%S").ok()?;
|
||||
let id = &value[id_start..];
|
||||
uuid::Uuid::try_parse(id).ok()?;
|
||||
Some(id.to_owned())
|
||||
}
|
||||
|
||||
fn title(primary: &str, fallback: &str) -> Option<String> {
|
||||
normalize_title(primary).or_else(|| normalize_title(fallback))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod fixed_path_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn fixed_rollout_qualification_does_not_require_enumeration() {
|
||||
let (_tempdir, root) = crate::foreign_sessions::canonical_tempdir();
|
||||
let sessions = root.join("sessions/2027/01/15");
|
||||
std::fs::create_dir_all(&sessions).unwrap();
|
||||
let id = uuid::Uuid::from_u128(9_000);
|
||||
let rollout = sessions.join(format!("rollout-2027-01-15T12-00-00-{id}.jsonl"));
|
||||
std::fs::write(&rollout, "").unwrap();
|
||||
let approved = ApprovedRoot::new(&root).unwrap();
|
||||
assert_eq!(
|
||||
existing_rollout_path(&approved, &rollout.to_string_lossy(), &id.to_string()),
|
||||
Some(dunce::canonicalize(&rollout).unwrap())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
mod tests;
|
||||
@@ -0,0 +1,889 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn codex_query_uses_cwd_index_and_length_metadata_opcode() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let db_path = root.path().join("state.sqlite");
|
||||
create_db(&db_path, &[]);
|
||||
let connection = Connection::open(&db_path).unwrap();
|
||||
let columns = [
|
||||
"id",
|
||||
"rollout_path",
|
||||
"updated_at_ms",
|
||||
"source",
|
||||
"cwd",
|
||||
"title",
|
||||
"first_user_message",
|
||||
"archived",
|
||||
"git_branch",
|
||||
]
|
||||
.into_iter()
|
||||
.map(str::to_owned)
|
||||
.collect::<std::collections::HashSet<_>>();
|
||||
let sql = super::super::db::scan_sql(&columns).unwrap();
|
||||
let mut plan = connection
|
||||
.prepare(&format!("EXPLAIN QUERY PLAN {sql}"))
|
||||
.unwrap();
|
||||
let details = plan
|
||||
.query_map(params!["/repo", 0_i64, i64::MAX], |row| {
|
||||
row.get::<_, String>(3)
|
||||
})
|
||||
.unwrap()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
assert!(details.iter().any(|detail| {
|
||||
detail.contains("SEARCH threads USING INDEX threads_archived_cwd_updated")
|
||||
&& detail.contains("archived=? AND cwd=?")
|
||||
}));
|
||||
assert!(
|
||||
details
|
||||
.iter()
|
||||
.any(|detail| detail.contains("USE TEMP B-TREE FOR ORDER BY"))
|
||||
);
|
||||
|
||||
let mut bytecode = connection.prepare(&format!("EXPLAIN {sql}")).unwrap();
|
||||
let columns = bytecode
|
||||
.query_map(params!["/repo", 0_i64, i64::MAX], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, i64>(3)?,
|
||||
row.get::<_, i64>(6)?,
|
||||
))
|
||||
})
|
||||
.unwrap()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
for column in [0_i64, 1, 5, 6, 8] {
|
||||
assert!(columns.iter().any(|(opcode, p2, p5)| {
|
||||
opcode == "Column" && *p2 == column && (*p5 & 0xc0) == 0xc0
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recent_database_probe_is_windowed_bounded_and_keeps_source_filters() {
|
||||
let (_tempdir, root) = canonical_tempdir();
|
||||
let cwd = root.join("repo");
|
||||
let rollout_dir = root.join("sessions/2027/01/15");
|
||||
fs::create_dir_all(&cwd).unwrap();
|
||||
fs::create_dir_all(&rollout_dir).unwrap();
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_000_000);
|
||||
let winner = uuid::Uuid::from_u128(90);
|
||||
let winner_rollout = rollout_path(&rollout_dir, winner);
|
||||
fs::write(&winner_rollout, "").unwrap();
|
||||
let missing = rollout_path(&rollout_dir, uuid::Uuid::from_u128(91));
|
||||
let db_path = root.join("state_9.sqlite");
|
||||
create_db(
|
||||
&db_path,
|
||||
&[
|
||||
DbRow {
|
||||
id: uuid::Uuid::from_u128(93),
|
||||
rollout_path: &winner_rollout,
|
||||
updated_at_ms: millis_from_system_time(now + Duration::from_secs(1)).unwrap(),
|
||||
source: r#"{"custom":"atlas"}"#,
|
||||
cwd: &cwd,
|
||||
title: "excluded custom source",
|
||||
first_user_message: "",
|
||||
archived: false,
|
||||
},
|
||||
DbRow {
|
||||
id: uuid::Uuid::from_u128(92),
|
||||
rollout_path: &winner_rollout,
|
||||
updated_at_ms: millis_from_system_time(now).unwrap(),
|
||||
source: r#"{"subagent":"review"}"#,
|
||||
cwd: &cwd,
|
||||
title: "excluded",
|
||||
first_user_message: "",
|
||||
archived: false,
|
||||
},
|
||||
DbRow {
|
||||
id: uuid::Uuid::from_u128(91),
|
||||
rollout_path: &missing,
|
||||
updated_at_ms: millis_from_system_time(now - Duration::from_secs(1)).unwrap(),
|
||||
source: "cli",
|
||||
cwd: &cwd,
|
||||
title: "missing rollout",
|
||||
first_user_message: "",
|
||||
archived: false,
|
||||
},
|
||||
DbRow {
|
||||
id: winner,
|
||||
rollout_path: &winner_rollout,
|
||||
updated_at_ms: millis_from_system_time(now - Duration::from_secs(2)).unwrap(),
|
||||
source: "vscode",
|
||||
cwd: &cwd,
|
||||
title: "",
|
||||
first_user_message: "",
|
||||
archived: false,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
let found = most_recent_in_home(&root, &cwd, now, Duration::from_secs(600)).unwrap();
|
||||
assert_eq!(found.native_id, winner.to_string());
|
||||
assert_eq!(found.source, ForeignSessionSource::CodexVsCode);
|
||||
|
||||
let columns = [
|
||||
"id",
|
||||
"rollout_path",
|
||||
"updated_at_ms",
|
||||
"source",
|
||||
"cwd",
|
||||
"title",
|
||||
"first_user_message",
|
||||
"archived",
|
||||
"git_branch",
|
||||
]
|
||||
.into_iter()
|
||||
.map(str::to_owned)
|
||||
.collect::<std::collections::HashSet<_>>();
|
||||
let sql = super::super::db::recent_scan_sql(&columns).unwrap();
|
||||
assert!(sql.contains("source IN ('cli', 'vscode')"));
|
||||
assert!(!sql.contains("custom"));
|
||||
assert!(sql.contains(&format!(
|
||||
"LIMIT {}",
|
||||
super::super::db::MAX_RECENT_DB_CANDIDATES + 1
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recent_database_sentinel_marks_invalid_window_incomplete() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let cwd = root.path().join("repo");
|
||||
let sessions = root.path().join("sessions");
|
||||
fs::create_dir_all(&cwd).unwrap();
|
||||
fs::create_dir_all(&sessions).unwrap();
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_010_000);
|
||||
let valid_id = uuid::Uuid::from_u128(190);
|
||||
let valid_rollout = rollout_path(&sessions, valid_id);
|
||||
fs::write(&valid_rollout, "").unwrap();
|
||||
let db_path = root.path().join("state_10.sqlite");
|
||||
create_db(&db_path, &[]);
|
||||
let connection = Connection::open(&db_path).unwrap();
|
||||
for index in 0..super::super::db::MAX_RECENT_DB_CANDIDATES {
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO threads VALUES (?1, ?2, ?3, 'cli', ?4, 'invalid', '', 0, NULL)",
|
||||
params![
|
||||
uuid::Uuid::from_u128(10_000 + index as u128).to_string(),
|
||||
sessions
|
||||
.join(format!("missing-{index}.jsonl"))
|
||||
.display()
|
||||
.to_string(),
|
||||
millis_from_system_time(now - Duration::from_secs(index as u64)).unwrap(),
|
||||
cwd.display().to_string(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(
|
||||
most_recent_in_home(root.path(), &cwd, now, Duration::from_secs(600)),
|
||||
RecentProbe::Complete(None),
|
||||
);
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO threads VALUES (?1, ?2, ?3, 'cli', ?4, 'ninth valid', '', 0, NULL)",
|
||||
params![
|
||||
valid_id.to_string(),
|
||||
valid_rollout.display().to_string(),
|
||||
millis_from_system_time(now - Duration::from_secs(20)).unwrap(),
|
||||
cwd.display().to_string(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
drop(connection);
|
||||
|
||||
assert_eq!(
|
||||
most_recent_in_home(root.path(), &cwd, now, Duration::from_secs(600)),
|
||||
RecentProbe::Incomplete,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recent_database_row_decode_error_is_incomplete() {
|
||||
let (_tempdir, root) = canonical_tempdir();
|
||||
let cwd = root.join("repo");
|
||||
let sessions = root.join("sessions");
|
||||
fs::create_dir_all(&cwd).unwrap();
|
||||
fs::create_dir_all(&sessions).unwrap();
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_015_000);
|
||||
let older_id = uuid::Uuid::from_u128(191);
|
||||
let older_rollout = rollout_path(&sessions, older_id);
|
||||
fs::write(&older_rollout, "").unwrap();
|
||||
let db_path = root.join("state_10.sqlite");
|
||||
create_db(
|
||||
&db_path,
|
||||
&[DbRow {
|
||||
id: older_id,
|
||||
rollout_path: &older_rollout,
|
||||
updated_at_ms: millis_from_system_time(now - Duration::from_secs(1)).unwrap(),
|
||||
source: "cli",
|
||||
cwd: &cwd,
|
||||
title: "older valid",
|
||||
first_user_message: "",
|
||||
archived: false,
|
||||
}],
|
||||
);
|
||||
let broken_id = uuid::Uuid::from_u128(192);
|
||||
let broken_rollout = rollout_path(&sessions, broken_id);
|
||||
fs::write(&broken_rollout, "").unwrap();
|
||||
let connection = Connection::open(&db_path).unwrap();
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO threads VALUES (?1, ?2, ?3, 'cli', ?4, CAST(x'80' AS TEXT), '', 0, NULL)",
|
||||
params![
|
||||
broken_id.to_string(),
|
||||
broken_rollout.display().to_string(),
|
||||
millis_from_system_time(now).unwrap(),
|
||||
cwd.display().to_string(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
drop(connection);
|
||||
|
||||
assert_eq!(
|
||||
most_recent_in_home(&root, &cwd, now, Duration::from_secs(600)),
|
||||
RecentProbe::Incomplete,
|
||||
);
|
||||
let full = scan_in_home(&root, &cwd, now);
|
||||
assert_eq!(full.len(), 1);
|
||||
assert_eq!(full[0].native_id, older_id.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recent_database_tri_state_uses_only_current_generation() {
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_020_000);
|
||||
|
||||
let readable = TempDir::new().unwrap();
|
||||
let readable_cwd = readable.path().join("repo");
|
||||
fs::create_dir_all(&readable_cwd).unwrap();
|
||||
let older_id = uuid::Uuid::from_u128(200);
|
||||
let older_rollout =
|
||||
write_recent_rollout(readable.path(), &readable_cwd, now, older_id, json!("cli"));
|
||||
create_db(
|
||||
&readable.path().join("state_9.sqlite"),
|
||||
&[DbRow {
|
||||
id: older_id,
|
||||
rollout_path: &older_rollout,
|
||||
updated_at_ms: millis_from_system_time(now).unwrap(),
|
||||
source: "cli",
|
||||
cwd: &readable_cwd,
|
||||
title: "obsolete generation",
|
||||
first_user_message: "",
|
||||
archived: false,
|
||||
}],
|
||||
);
|
||||
create_db(&readable.path().join("state_10.sqlite"), &[]);
|
||||
assert!(
|
||||
most_recent_in_home(
|
||||
readable.path(),
|
||||
&readable_cwd,
|
||||
now,
|
||||
Duration::from_secs(600)
|
||||
)
|
||||
.is_none(),
|
||||
"a usable empty current index must suppress obsolete DB and rollout fallback"
|
||||
);
|
||||
|
||||
let unreadable = TempDir::new().unwrap();
|
||||
let unreadable_cwd = unreadable.path().join("repo");
|
||||
fs::create_dir_all(&unreadable_cwd).unwrap();
|
||||
let fallback_id = uuid::Uuid::from_u128(201);
|
||||
write_recent_rollout(
|
||||
unreadable.path(),
|
||||
&unreadable_cwd,
|
||||
now,
|
||||
fallback_id,
|
||||
json!("vscode"),
|
||||
);
|
||||
fs::write(unreadable.path().join("state_10.sqlite"), "not sqlite").unwrap();
|
||||
assert_eq!(
|
||||
most_recent_in_home(
|
||||
unreadable.path(),
|
||||
&unreadable_cwd,
|
||||
now,
|
||||
Duration::from_secs(600)
|
||||
)
|
||||
.unwrap()
|
||||
.native_id,
|
||||
fallback_id.to_string()
|
||||
);
|
||||
|
||||
let absent = TempDir::new().unwrap();
|
||||
let absent_cwd = absent.path().join("repo");
|
||||
fs::create_dir_all(&absent_cwd).unwrap();
|
||||
let absent_id = uuid::Uuid::from_u128(202);
|
||||
write_recent_rollout(absent.path(), &absent_cwd, now, absent_id, json!("cli"));
|
||||
assert_eq!(
|
||||
most_recent_in_home(absent.path(), &absent_cwd, now, Duration::from_secs(600))
|
||||
.unwrap()
|
||||
.native_id,
|
||||
absent_id.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn recent_database_unsafe_highest_uses_fallback_not_older_database() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let cwd = root.path().join("repo");
|
||||
fs::create_dir_all(&cwd).unwrap();
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_025_000);
|
||||
let older_id = uuid::Uuid::from_u128(210);
|
||||
let older_rollout = write_recent_rollout(root.path(), &cwd, now, older_id, json!("cli"));
|
||||
touch(&older_rollout, now - Duration::from_secs(60 * 60));
|
||||
let older_db = root.path().join("state_9.sqlite");
|
||||
create_db(
|
||||
&older_db,
|
||||
&[DbRow {
|
||||
id: older_id,
|
||||
rollout_path: &older_rollout,
|
||||
updated_at_ms: millis_from_system_time(now).unwrap(),
|
||||
source: "cli",
|
||||
cwd: &cwd,
|
||||
title: "must not use older DB",
|
||||
first_user_message: "",
|
||||
archived: false,
|
||||
}],
|
||||
);
|
||||
std::os::unix::fs::symlink(&older_db, root.path().join("state_10.sqlite")).unwrap();
|
||||
|
||||
let fallback_id = uuid::Uuid::from_u128(211);
|
||||
write_recent_rollout(root.path(), &cwd, now, fallback_id, json!("vscode"));
|
||||
assert_eq!(
|
||||
most_recent_in_home(root.path(), &cwd, now, Duration::from_secs(600))
|
||||
.unwrap()
|
||||
.native_id,
|
||||
fallback_id.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn highest_database_filters_sources_paths_cwd_and_millis() {
|
||||
let (_tempdir, root) = canonical_tempdir();
|
||||
let cwd = root.join("repo");
|
||||
fs::create_dir_all(&cwd).unwrap();
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_000_000);
|
||||
let updated = now - Duration::from_secs(2);
|
||||
let updated_ms = millis_from_system_time(updated).unwrap();
|
||||
let rollout_dir = root.join("sessions/2026/01/01");
|
||||
fs::create_dir_all(&rollout_dir).unwrap();
|
||||
let old_rollout = rollout_path(&rollout_dir, uuid::Uuid::from_u128(1));
|
||||
fs::write(&old_rollout, "").unwrap();
|
||||
create_db(
|
||||
&root.join("state_5.sqlite"),
|
||||
&[DbRow {
|
||||
id: uuid::Uuid::from_u128(1),
|
||||
rollout_path: &old_rollout,
|
||||
updated_at_ms: updated_ms,
|
||||
source: "cli",
|
||||
cwd: &cwd,
|
||||
title: "old generation",
|
||||
first_user_message: "",
|
||||
archived: false,
|
||||
}],
|
||||
);
|
||||
|
||||
let rollout = rollout_path(&rollout_dir, uuid::Uuid::from_u128(2));
|
||||
fs::write(&rollout, "").unwrap();
|
||||
let compressed_plain = rollout_path(&rollout_dir, uuid::Uuid::from_u128(3));
|
||||
fs::write(format!("{}.zst", compressed_plain.display()), "").unwrap();
|
||||
let chatgpt_rollout = rollout_path(&rollout_dir, uuid::Uuid::from_u128(9));
|
||||
fs::write(&chatgpt_rollout, "").unwrap();
|
||||
let missing = rollout_path(&rollout_dir, uuid::Uuid::from_u128(5));
|
||||
let stale_ms = millis_from_system_time(
|
||||
now - super::super::super::MAX_SESSION_AGE - Duration::from_secs(1),
|
||||
)
|
||||
.unwrap();
|
||||
create_db(
|
||||
&root.join("state_9.sqlite"),
|
||||
&[
|
||||
DbRow {
|
||||
id: uuid::Uuid::from_u128(2),
|
||||
rollout_path: &rollout,
|
||||
updated_at_ms: updated_ms,
|
||||
source: "vscode",
|
||||
cwd: &cwd,
|
||||
title: "",
|
||||
first_user_message: "fallback title",
|
||||
archived: false,
|
||||
},
|
||||
DbRow {
|
||||
id: uuid::Uuid::from_u128(3),
|
||||
rollout_path: &compressed_plain,
|
||||
updated_at_ms: updated_ms - 1,
|
||||
source: r#"{"custom":"atlas"}"#,
|
||||
cwd: &cwd,
|
||||
title: "compressed",
|
||||
first_user_message: "",
|
||||
archived: false,
|
||||
},
|
||||
DbRow {
|
||||
id: uuid::Uuid::from_u128(9),
|
||||
rollout_path: &chatgpt_rollout,
|
||||
updated_at_ms: updated_ms - 2,
|
||||
source: r#"{"custom":"chatgpt"}"#,
|
||||
cwd: &cwd,
|
||||
title: "chatgpt",
|
||||
first_user_message: "",
|
||||
archived: false,
|
||||
},
|
||||
DbRow {
|
||||
id: uuid::Uuid::from_u128(4),
|
||||
rollout_path: &rollout,
|
||||
updated_at_ms: updated_ms,
|
||||
source: r#"{"subagent":"review"}"#,
|
||||
cwd: &cwd,
|
||||
title: "subagent",
|
||||
first_user_message: "",
|
||||
archived: false,
|
||||
},
|
||||
DbRow {
|
||||
id: uuid::Uuid::from_u128(5),
|
||||
rollout_path: &missing,
|
||||
updated_at_ms: updated_ms,
|
||||
source: "cli",
|
||||
cwd: &cwd,
|
||||
title: "missing",
|
||||
first_user_message: "",
|
||||
archived: false,
|
||||
},
|
||||
DbRow {
|
||||
id: uuid::Uuid::from_u128(6),
|
||||
rollout_path: &rollout,
|
||||
updated_at_ms: stale_ms,
|
||||
source: "cli",
|
||||
cwd: &cwd,
|
||||
title: "stale",
|
||||
first_user_message: "",
|
||||
archived: false,
|
||||
},
|
||||
DbRow {
|
||||
id: uuid::Uuid::from_u128(7),
|
||||
rollout_path: &rollout,
|
||||
updated_at_ms: updated_ms,
|
||||
source: "cli",
|
||||
cwd: Path::new("/other"),
|
||||
title: "wrong cwd",
|
||||
first_user_message: "",
|
||||
archived: false,
|
||||
},
|
||||
DbRow {
|
||||
id: uuid::Uuid::from_u128(8),
|
||||
rollout_path: &rollout,
|
||||
updated_at_ms: updated_ms,
|
||||
source: "cli",
|
||||
cwd: &cwd,
|
||||
title: "archived",
|
||||
first_user_message: "",
|
||||
archived: true,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
let sessions = scan_in_home(&root, &cwd, now);
|
||||
assert_eq!(
|
||||
sessions
|
||||
.iter()
|
||||
.map(|session| session.title.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["fallback title", "compressed", "chatgpt"]
|
||||
);
|
||||
assert_eq!(sessions[0].updated_at, updated);
|
||||
assert_eq!(sessions[0].source, ForeignSessionSource::CodexVsCode);
|
||||
assert_eq!(sessions[1].source, ForeignSessionSource::CodexAtlas);
|
||||
assert_eq!(sessions[2].source, ForeignSessionSource::CodexChatGpt);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_newer_database_uses_older_nonempty_generation() {
|
||||
let (_tempdir, root) = canonical_tempdir();
|
||||
let cwd = root.join("repo");
|
||||
let rollout_dir = root.join("sessions/2027/01/15");
|
||||
fs::create_dir_all(&cwd).unwrap();
|
||||
fs::create_dir_all(&rollout_dir).unwrap();
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_050_000);
|
||||
let id = uuid::Uuid::from_u128(40);
|
||||
let rollout = rollout_path(&rollout_dir, id);
|
||||
fs::write(&rollout, "").unwrap();
|
||||
create_db(
|
||||
&root.join("state_9.sqlite"),
|
||||
&[DbRow {
|
||||
id,
|
||||
rollout_path: &rollout,
|
||||
updated_at_ms: millis_from_system_time(now).unwrap(),
|
||||
source: "cli",
|
||||
cwd: &cwd,
|
||||
title: "older nonempty generation",
|
||||
first_user_message: "",
|
||||
archived: false,
|
||||
}],
|
||||
);
|
||||
create_db(&root.join("state_10.sqlite"), &[]);
|
||||
|
||||
let sessions = scan_in_home(&root, &cwd, now);
|
||||
|
||||
assert_eq!(sessions.len(), 1);
|
||||
assert_eq!(sessions[0].native_id, id.to_string());
|
||||
assert_eq!(sessions[0].title, "older nonempty generation");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_databases_fall_back_to_rollout_files() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let cwd = root.path().join("repo");
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_100_000);
|
||||
let day = session_day(root.path(), now);
|
||||
fs::create_dir_all(&cwd).unwrap();
|
||||
fs::create_dir_all(&day).unwrap();
|
||||
let id = uuid::Uuid::from_u128(41);
|
||||
let rollout = rollout_path(&day, id);
|
||||
let contents = [
|
||||
json!({
|
||||
"type": "session_meta",
|
||||
"payload": {
|
||||
"id": id,
|
||||
"cwd": cwd.display().to_string(),
|
||||
"source": "cli"
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"type": "event_msg",
|
||||
"payload": {
|
||||
"type": "user_message",
|
||||
"message": "rollout fallback"
|
||||
}
|
||||
}),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|record| record.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
fs::write(&rollout, contents).unwrap();
|
||||
touch(&rollout, now);
|
||||
create_db(&root.path().join("state_10.sqlite"), &[]);
|
||||
|
||||
let sessions = scan_in_home(root.path(), &cwd, now);
|
||||
|
||||
assert_eq!(sessions.len(), 1);
|
||||
assert_eq!(sessions[0].native_id, id.to_string());
|
||||
assert_eq!(sessions[0].title, "rollout fallback");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn database_window_qualifies_past_invalid_rows() {
|
||||
let (_tempdir, root) = canonical_tempdir();
|
||||
let cwd = root.join("repo");
|
||||
let sessions = root.join("sessions");
|
||||
fs::create_dir_all(&cwd).unwrap();
|
||||
fs::create_dir_all(&sessions).unwrap();
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_200_000);
|
||||
let valid_id = uuid::Uuid::from_u128(500);
|
||||
let valid_path = rollout_path(&sessions, valid_id);
|
||||
fs::write(&valid_path, "").unwrap();
|
||||
let db_path = root.join("state_10.sqlite");
|
||||
create_db(
|
||||
&db_path,
|
||||
&[DbRow {
|
||||
id: valid_id,
|
||||
rollout_path: &valid_path,
|
||||
updated_at_ms: millis_from_system_time(now - Duration::from_secs(100)).unwrap(),
|
||||
source: "cli",
|
||||
cwd: &cwd,
|
||||
title: "valid after window",
|
||||
first_user_message: "",
|
||||
archived: false,
|
||||
}],
|
||||
);
|
||||
let connection = Connection::open(&db_path).unwrap();
|
||||
for i in 0..60_u128 {
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO threads VALUES (?1, ?2, ?3, 'cli', ?4, 'invalid', '', 0, NULL)",
|
||||
params![
|
||||
uuid::Uuid::from_u128(1_000 + i).to_string(),
|
||||
sessions
|
||||
.join(format!("missing-{i}.jsonl"))
|
||||
.display()
|
||||
.to_string(),
|
||||
millis_from_system_time(now - Duration::from_secs(i as u64)).unwrap(),
|
||||
cwd.display().to_string(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
for i in 0..220_u128 {
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO threads VALUES (?1, ?2, ?3, ?4, ?5, 'subagent', '', 0, NULL)",
|
||||
params![
|
||||
uuid::Uuid::from_u128(2_000 + i).to_string(),
|
||||
valid_path.display().to_string(),
|
||||
millis_from_system_time(now - Duration::from_secs(i as u64)).unwrap(),
|
||||
r#"{"subagent":"review"}"#,
|
||||
cwd.display().to_string(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
drop(connection);
|
||||
let found = scan_in_home(&root, &cwd, now);
|
||||
assert_eq!(found.len(), 1);
|
||||
assert_eq!(found[0].title, "valid after window");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn database_window_normalizes_units_and_filters_future_rows_before_limit() {
|
||||
let (_tempdir, root) = canonical_tempdir();
|
||||
let cwd = root.join("repo");
|
||||
let sessions = root.join("sessions");
|
||||
fs::create_dir_all(&cwd).unwrap();
|
||||
fs::create_dir_all(&sessions).unwrap();
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_250_000);
|
||||
let valid_id = uuid::Uuid::from_u128(2_500);
|
||||
let valid_path = rollout_path(&sessions, valid_id);
|
||||
fs::write(&valid_path, "").unwrap();
|
||||
let db_path = root.join("state_10.sqlite");
|
||||
create_db(
|
||||
&db_path,
|
||||
&[DbRow {
|
||||
id: valid_id,
|
||||
rollout_path: &valid_path,
|
||||
updated_at_ms: i64::try_from(
|
||||
(now - Duration::from_secs(1))
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
)
|
||||
.unwrap(),
|
||||
source: "cli",
|
||||
cwd: &cwd,
|
||||
title: "newer seconds row",
|
||||
first_user_message: "",
|
||||
archived: false,
|
||||
}],
|
||||
);
|
||||
let connection = Connection::open(&db_path).unwrap();
|
||||
for (prefix, offset) in [
|
||||
("stale", -(10 * 24 * 60 * 60_i64)),
|
||||
("future", 24 * 60 * 60),
|
||||
] {
|
||||
for i in 0..205_u128 {
|
||||
let timestamp = if offset < 0 {
|
||||
now - Duration::from_secs((-offset) as u64 + i as u64)
|
||||
} else {
|
||||
now + Duration::from_secs(offset as u64 + i as u64)
|
||||
};
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO threads VALUES (?1, ?2, ?3, 'cli', ?4, ?5, '', 0, NULL)",
|
||||
params![
|
||||
uuid::Uuid::from_u128(3_000 + i + if offset > 0 { 1_000 } else { 0 })
|
||||
.to_string(),
|
||||
sessions
|
||||
.join(format!("missing-{prefix}-{i}.jsonl"))
|
||||
.display()
|
||||
.to_string(),
|
||||
millis_from_system_time(timestamp).unwrap(),
|
||||
cwd.display().to_string(),
|
||||
prefix,
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
drop(connection);
|
||||
let found = scan_in_home(&root, &cwd, now);
|
||||
assert_eq!(found.len(), 1);
|
||||
assert_eq!(found[0].title, "newer seconds row");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_required_sql_predicate_protects_the_candidate_window() {
|
||||
for case in 0..5_u128 {
|
||||
let (_tempdir, root) = canonical_tempdir();
|
||||
let cwd = root.join("repo");
|
||||
let sessions = root.join("sessions");
|
||||
fs::create_dir_all(&cwd).unwrap();
|
||||
fs::create_dir_all(&sessions).unwrap();
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_260_000 + case as u64);
|
||||
let valid_id = uuid::Uuid::from_u128(6_000 + case);
|
||||
let valid_path = rollout_path(&sessions, valid_id);
|
||||
fs::write(&valid_path, "").unwrap();
|
||||
let db_path = root.join("state_10.sqlite");
|
||||
create_db(
|
||||
&db_path,
|
||||
&[DbRow {
|
||||
id: valid_id,
|
||||
rollout_path: &valid_path,
|
||||
updated_at_ms: millis_from_system_time(now - Duration::from_secs(1)).unwrap(),
|
||||
source: "cli",
|
||||
cwd: &cwd,
|
||||
title: "valid required row",
|
||||
first_user_message: "",
|
||||
archived: false,
|
||||
}],
|
||||
);
|
||||
let connection = Connection::open(&db_path).unwrap();
|
||||
for i in 0..201_u128 {
|
||||
let id_text = uuid::Uuid::from_u128(7_000 + i).to_string();
|
||||
let mut id = SqlValue::Text(id_text.clone());
|
||||
let rollout_text = sessions
|
||||
.join(format!("missing-required-{case}-{i}.jsonl"))
|
||||
.display()
|
||||
.to_string();
|
||||
let mut rollout = SqlValue::Text(rollout_text.clone());
|
||||
let mut updated = SqlValue::Integer(millis_from_system_time(now).unwrap());
|
||||
let source = SqlValue::Text("cli".to_owned());
|
||||
let stored_cwd = SqlValue::Text(cwd.display().to_string());
|
||||
match case {
|
||||
0 => id = SqlValue::Blob(id_text.into_bytes()),
|
||||
1 => id = SqlValue::Text(oversized_text(64, i % 2 == 1)),
|
||||
2 => rollout = SqlValue::Blob(rollout_text.into_bytes()),
|
||||
3 => rollout = SqlValue::Text(oversized_text(16 * 1024, i % 2 == 1)),
|
||||
_ => updated = SqlValue::Real(millis_from_system_time(now).unwrap() as f64 + 0.5),
|
||||
}
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO threads VALUES (?1, ?2, ?3, ?4, ?5, 'invalid', '', 0, NULL)",
|
||||
params![id, rollout, updated, source, stored_cwd],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
drop(connection);
|
||||
let found = scan_in_home(&root, &cwd, now);
|
||||
assert_eq!(found.len(), 1, "required predicate case {case}");
|
||||
assert_eq!(found[0].native_id, valid_id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_codex_metadata_degrades_without_dropping_rows() {
|
||||
let (_tempdir, root) = canonical_tempdir();
|
||||
let cwd = root.join("repo");
|
||||
let sessions = root.join("sessions");
|
||||
fs::create_dir_all(&cwd).unwrap();
|
||||
fs::create_dir_all(&sessions).unwrap();
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_270_000);
|
||||
let db_path = root.join("state_10.sqlite");
|
||||
create_db(&db_path, &[]);
|
||||
let connection = Connection::open(&db_path).unwrap();
|
||||
let expected = [
|
||||
"fallback oversized title",
|
||||
"fallback wrong title type",
|
||||
"title with oversized fallback",
|
||||
"title with wrong fallback type",
|
||||
"title with oversized branch",
|
||||
"title with wrong branch type",
|
||||
];
|
||||
for (index, expected_title) in expected.iter().enumerate() {
|
||||
let id = uuid::Uuid::from_u128(8_000 + index as u128);
|
||||
let path = rollout_path(&sessions, id);
|
||||
fs::write(&path, "").unwrap();
|
||||
let mut title = SqlValue::Text((*expected_title).to_owned());
|
||||
let mut first = SqlValue::Text((*expected_title).to_owned());
|
||||
let mut branch = SqlValue::Text("main".to_owned());
|
||||
match index {
|
||||
0 => title = SqlValue::Text(oversized_text(64 * 1024, false)),
|
||||
1 => title = SqlValue::Blob(b"wrong title".to_vec()),
|
||||
2 => first = SqlValue::Text(oversized_text(64 * 1024, true)),
|
||||
3 => first = SqlValue::Blob(b"wrong fallback".to_vec()),
|
||||
4 => branch = SqlValue::Text(oversized_text(4 * 1024, false)),
|
||||
_ => branch = SqlValue::Blob(b"wrong branch".to_vec()),
|
||||
}
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO threads VALUES (?1, ?2, ?3, 'cli', ?4, ?5, ?6, 0, ?7)",
|
||||
params![
|
||||
id.to_string(),
|
||||
path.display().to_string(),
|
||||
millis_from_system_time(now - Duration::from_secs(index as u64)).unwrap(),
|
||||
cwd.display().to_string(),
|
||||
title,
|
||||
first,
|
||||
branch,
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
drop(connection);
|
||||
let found = scan_in_home(&root, &cwd, now);
|
||||
assert_eq!(found.len(), expected.len());
|
||||
for (index, expected_title) in expected.iter().enumerate() {
|
||||
let id = uuid::Uuid::from_u128(8_000 + index as u128).to_string();
|
||||
let session = found
|
||||
.iter()
|
||||
.find(|session| session.native_id == id)
|
||||
.unwrap();
|
||||
assert_eq!(session.title, *expected_title);
|
||||
if index >= 4 {
|
||||
assert_eq!(session.branch, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_database_probes_have_a_supported_generation_ceiling() {
|
||||
let (_tempdir, root) = canonical_tempdir();
|
||||
for i in 0..100 {
|
||||
fs::write(root.join(format!("unrelated-{i:03}")), "").unwrap();
|
||||
}
|
||||
fs::write(root.join("state_2.sqlite"), "").unwrap();
|
||||
let boundary = root.join(format!("state_{MAX_STATE_DB_GENERATION}.sqlite"));
|
||||
let beyond = root.join(format!("state_{}.sqlite", MAX_STATE_DB_GENERATION + 1));
|
||||
fs::write(&boundary, "").unwrap();
|
||||
fs::write(&beyond, "").unwrap();
|
||||
let approved_root = ApprovedRoot::new(&root).unwrap();
|
||||
assert_eq!(
|
||||
state_databases(&approved_root).collect::<Vec<_>>(),
|
||||
vec![boundary.clone(), root.join("state_2.sqlite")]
|
||||
);
|
||||
fs::remove_file(boundary).unwrap();
|
||||
assert_eq!(
|
||||
state_databases(&approved_root).collect::<Vec<_>>(),
|
||||
vec![root.join("state_2.sqlite")]
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn state_database_probe_rejects_symlink_escape() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let codex_home = root.path().join("codex");
|
||||
fs::create_dir_all(&codex_home).unwrap();
|
||||
let outside = root.path().join("outside.sqlite");
|
||||
fs::write(&outside, "").unwrap();
|
||||
std::os::unix::fs::symlink(
|
||||
&outside,
|
||||
codex_home.join(format!("state_{MAX_STATE_DB_GENERATION}.sqlite")),
|
||||
)
|
||||
.unwrap();
|
||||
let approved_root = ApprovedRoot::new(&codex_home).unwrap();
|
||||
assert!(state_databases(&approved_root).next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_legacy_seconds_and_millis_then_uses_shared_recency() {
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_000_000);
|
||||
let expected = now - Duration::from_secs(10);
|
||||
let seconds = 1_799_999_990_i64;
|
||||
let millis = millis_from_system_time(expected).unwrap();
|
||||
assert_eq!(
|
||||
super::super::db::normalize_updated_at(seconds),
|
||||
Some(expected)
|
||||
);
|
||||
assert_eq!(
|
||||
super::super::db::normalize_updated_at(millis),
|
||||
Some(expected)
|
||||
);
|
||||
let future = now + Duration::from_secs(24 * 60 * 60);
|
||||
assert_eq!(
|
||||
super::super::db::normalize_updated_at(millis_from_system_time(future).unwrap()),
|
||||
Some(future)
|
||||
);
|
||||
assert!(!super::super::super::is_within(
|
||||
future,
|
||||
now,
|
||||
super::super::super::MAX_SESSION_AGE,
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
use super::*;
|
||||
|
||||
fn rollout_records(id: uuid::Uuid, cwd: &Path, source: serde_json::Value, title: &str) -> String {
|
||||
[
|
||||
json!({
|
||||
"type": "session_meta",
|
||||
"payload": {
|
||||
"id": id,
|
||||
"cwd": cwd.display().to_string(),
|
||||
"source": source
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"type": "event_msg",
|
||||
"payload": {"type": "user_message", "message": title}
|
||||
}),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|record| record.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recent_fallback_skips_excluded_sources_and_wrong_cwds() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let cwd = root.path().join("repo");
|
||||
fs::create_dir_all(&cwd).unwrap();
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_090_000);
|
||||
let day = session_day(root.path(), now);
|
||||
fs::create_dir_all(&day).unwrap();
|
||||
let excluded = uuid::Uuid::from_u128(10);
|
||||
let custom = uuid::Uuid::from_u128(13);
|
||||
let wrong_cwd = uuid::Uuid::from_u128(11);
|
||||
let winner = uuid::Uuid::from_u128(12);
|
||||
for (id, stored_cwd, source, age) in [
|
||||
(
|
||||
excluded,
|
||||
cwd.as_path(),
|
||||
json!({"subagent":"review"}),
|
||||
Duration::ZERO,
|
||||
),
|
||||
(
|
||||
custom,
|
||||
cwd.as_path(),
|
||||
json!({"custom":"atlas"}),
|
||||
Duration::from_millis(500),
|
||||
),
|
||||
(
|
||||
wrong_cwd,
|
||||
Path::new("/other"),
|
||||
json!("cli"),
|
||||
Duration::from_secs(1),
|
||||
),
|
||||
(
|
||||
winner,
|
||||
cwd.as_path(),
|
||||
json!("vscode"),
|
||||
Duration::from_secs(2),
|
||||
),
|
||||
] {
|
||||
let path = day.join(format!("rollout-2027-01-15T12-00-00-{id}.jsonl"));
|
||||
fs::write(&path, rollout_records(id, stored_cwd, source, "")).unwrap();
|
||||
touch(&path, now - age);
|
||||
}
|
||||
|
||||
let found = most_recent_in_home(root.path(), &cwd, now, Duration::from_secs(600)).unwrap();
|
||||
assert_eq!(found.native_id, winner.to_string());
|
||||
assert_eq!(found.source, ForeignSessionSource::CodexVsCode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recent_fallback_fails_closed_at_directory_entry_cap() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let cwd = root.path().join("repo");
|
||||
fs::create_dir_all(&cwd).unwrap();
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_095_000);
|
||||
let day = session_day(root.path(), now);
|
||||
fs::create_dir_all(&day).unwrap();
|
||||
write_recent_rollout(
|
||||
root.path(),
|
||||
&cwd,
|
||||
now,
|
||||
uuid::Uuid::from_u128(15),
|
||||
json!("cli"),
|
||||
);
|
||||
for index in 0..super::super::files::MAX_RECENT_DIRECTORY_ENTRIES {
|
||||
fs::write(day.join(format!("junk-{index:03}")), "").unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
most_recent_in_home(root.path(), &cwd, now, Duration::from_secs(600)),
|
||||
RecentProbe::Incomplete,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recent_fallback_includes_old_creation_directory_with_fresh_mtime() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let cwd = root.path().join("repo");
|
||||
fs::create_dir_all(&cwd).unwrap();
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_097_000);
|
||||
let created_at = now - Duration::from_secs(10 * 24 * 60 * 60);
|
||||
let day = session_day(root.path(), created_at);
|
||||
fs::create_dir_all(&day).unwrap();
|
||||
let id = uuid::Uuid::from_u128(16);
|
||||
let path = rollout_path(&day, id);
|
||||
fs::write(&path, rollout_records(id, &cwd, json!("cli"), "")).unwrap();
|
||||
touch(&path, now);
|
||||
|
||||
assert_eq!(
|
||||
most_recent_in_home(root.path(), &cwd, now, Duration::from_secs(600))
|
||||
.unwrap()
|
||||
.native_id,
|
||||
id.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn falls_back_to_bounded_plain_and_compressed_heads() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let cwd = root.path().join("repo");
|
||||
fs::create_dir_all(&cwd).unwrap();
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_100_000);
|
||||
let day = session_day(root.path(), now);
|
||||
let valid_day = session_day(root.path(), now - Duration::from_secs(24 * 60 * 60));
|
||||
fs::create_dir_all(&day).unwrap();
|
||||
fs::create_dir_all(&valid_day).unwrap();
|
||||
|
||||
let plain_id = uuid::Uuid::from_u128(20);
|
||||
let plain = valid_day.join(format!("rollout-1970-05-08T12-00-00-{plain_id}.jsonl"));
|
||||
fs::write(
|
||||
&plain,
|
||||
format!(
|
||||
"{{malformed\n{}",
|
||||
rollout_records(plain_id, &cwd, json!("cli"), "filesystem title")
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
touch(&plain, now);
|
||||
|
||||
let compressed_id = uuid::Uuid::from_u128(21);
|
||||
let compressed = valid_day.join(format!(
|
||||
"rollout-1970-05-08T12-00-01-{compressed_id}.jsonl.zst"
|
||||
));
|
||||
let compressed_head = rollout_records(compressed_id, &cwd, json!("vscode"), "compressed title");
|
||||
fs::write(
|
||||
&compressed,
|
||||
zstd::encode_all(compressed_head.as_bytes(), 1).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
touch(&compressed, now - Duration::from_secs(1));
|
||||
|
||||
let malformed_id = uuid::Uuid::from_u128(22);
|
||||
let malformed = valid_day.join(format!(
|
||||
"rollout-1970-05-08T12-00-02-{malformed_id}.jsonl.zst"
|
||||
));
|
||||
fs::write(&malformed, b"not a zstd frame").unwrap();
|
||||
touch(&malformed, now - Duration::from_secs(2));
|
||||
|
||||
let concatenated_id = uuid::Uuid::from_u128(23);
|
||||
let concatenated_path = valid_day.join(format!(
|
||||
"rollout-1970-05-08T12-00-03-{concatenated_id}.jsonl.zst"
|
||||
));
|
||||
let mut concatenated = zstd::encode_all(
|
||||
json!({"type":"event_msg","payload":{"type":"user_message","message":"frame one"}})
|
||||
.to_string()
|
||||
.as_bytes(),
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
concatenated.extend(
|
||||
zstd::encode_all(
|
||||
rollout_records(
|
||||
concatenated_id,
|
||||
&cwd,
|
||||
json!("cli"),
|
||||
"must not read frame two",
|
||||
)
|
||||
.as_bytes(),
|
||||
1,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
fs::write(&concatenated_path, concatenated).unwrap();
|
||||
touch(&concatenated_path, now - Duration::from_secs(3));
|
||||
|
||||
let skippable_id = uuid::Uuid::from_u128(24);
|
||||
let skippable_path = valid_day.join(format!(
|
||||
"rollout-1970-05-08T12-00-04-{skippable_id}.jsonl.zst"
|
||||
));
|
||||
let payload_len = super::super::files::MAX_COMPRESSED_HEAD_BYTES + 1024;
|
||||
let mut skippable = Vec::with_capacity(payload_len + 8);
|
||||
skippable.extend(0x184D_2A50_u32.to_le_bytes());
|
||||
skippable.extend(u32::try_from(payload_len).unwrap().to_le_bytes());
|
||||
skippable.resize(8 + payload_len, 0);
|
||||
skippable.extend(
|
||||
zstd::encode_all(
|
||||
rollout_records(skippable_id, &cwd, json!("cli"), "beyond compressed cap").as_bytes(),
|
||||
1,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
fs::write(&skippable_path, &skippable).unwrap();
|
||||
assert!(skippable.len() > super::super::files::MAX_COMPRESSED_HEAD_BYTES);
|
||||
touch(&skippable_path, now - Duration::from_secs(4));
|
||||
|
||||
let window_id = uuid::Uuid::from_u128(25);
|
||||
let window_path = valid_day.join(format!("rollout-1970-05-08T12-00-05-{window_id}.jsonl.zst"));
|
||||
let window_head = rollout_records(window_id, &cwd, json!("cli"), "oversized window");
|
||||
let mut encoder = zstd::Encoder::new(Vec::new(), 1).unwrap();
|
||||
encoder
|
||||
.window_log(super::super::files::MAX_ZSTD_WINDOW_LOG + 1)
|
||||
.unwrap();
|
||||
encoder.include_contentsize(false).unwrap();
|
||||
encoder.write_all(window_head.as_bytes()).unwrap();
|
||||
let window_frame = encoder.finish().unwrap();
|
||||
assert_eq!(
|
||||
zstd::decode_all(window_frame.as_slice()).unwrap(),
|
||||
window_head.as_bytes()
|
||||
);
|
||||
let mut limited = zstd::Decoder::new(window_frame.as_slice()).unwrap();
|
||||
limited
|
||||
.window_log_max(super::super::files::MAX_ZSTD_WINDOW_LOG)
|
||||
.unwrap();
|
||||
let mut limited_output = Vec::new();
|
||||
assert!(limited.read_to_end(&mut limited_output).is_err());
|
||||
fs::write(&window_path, window_frame).unwrap();
|
||||
touch(&window_path, now - Duration::from_secs(5));
|
||||
|
||||
let output_id = uuid::Uuid::from_u128(26);
|
||||
let output_path = valid_day.join(format!("rollout-1970-05-08T12-00-06-{output_id}.jsonl.zst"));
|
||||
let output_head = format!(
|
||||
"{}\n{}",
|
||||
"x".repeat(super::super::files::MAX_HEAD_BYTES + 1),
|
||||
rollout_records(output_id, &cwd, json!("cli"), "beyond output cap")
|
||||
);
|
||||
let output_frame = zstd::encode_all(output_head.as_bytes(), 1).unwrap();
|
||||
assert!(
|
||||
String::from_utf8(zstd::decode_all(output_frame.as_slice()).unwrap())
|
||||
.unwrap()
|
||||
.contains(&output_id.to_string())
|
||||
);
|
||||
fs::write(&output_path, output_frame).unwrap();
|
||||
touch(&output_path, now - Duration::from_secs(6));
|
||||
|
||||
for i in 0..520_u128 {
|
||||
let id = uuid::Uuid::from_u128(100 + i);
|
||||
let path = day.join(format!("rollout-1970-05-08T12-01-00-{id}.jsonl"));
|
||||
fs::write(
|
||||
&path,
|
||||
rollout_records(id, &cwd, json!({"subagent":"review"}), "excluded source"),
|
||||
)
|
||||
.unwrap();
|
||||
touch(&path, now - Duration::from_secs(i as u64 + 1));
|
||||
}
|
||||
|
||||
let sessions = scan_in_home(root.path(), &cwd, now);
|
||||
assert_eq!(sessions.len(), 2);
|
||||
assert_eq!(sessions[0].native_id, plain_id.to_string());
|
||||
assert_eq!(sessions[0].title, "filesystem title");
|
||||
assert_eq!(sessions[1].native_id, compressed_id.to_string());
|
||||
assert_eq!(sessions[1].title, "compressed title");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_requires_complete_matching_first_session_meta() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let cwd = root.path().join("repo");
|
||||
fs::create_dir_all(&cwd).unwrap();
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_150_000);
|
||||
let day = session_day(root.path(), now);
|
||||
fs::create_dir_all(&day).unwrap();
|
||||
for (id, first_payload) in [
|
||||
(
|
||||
uuid::Uuid::from_u128(30),
|
||||
json!({"id":uuid::Uuid::from_u128(30),"source":"cli"}),
|
||||
),
|
||||
(
|
||||
uuid::Uuid::from_u128(31),
|
||||
json!({"id":uuid::Uuid::from_u128(31),"cwd":cwd.display().to_string()}),
|
||||
),
|
||||
(
|
||||
uuid::Uuid::from_u128(32),
|
||||
json!({"cwd":cwd.display().to_string(),"source":"cli"}),
|
||||
),
|
||||
(
|
||||
uuid::Uuid::from_u128(33),
|
||||
json!({"id":uuid::Uuid::from_u128(999),"cwd":cwd.display().to_string(),"source":"cli"}),
|
||||
),
|
||||
] {
|
||||
let path = day.join(format!("rollout-2027-01-15T12-00-00-{id}.jsonl"));
|
||||
fs::write(
|
||||
&path,
|
||||
[
|
||||
json!({"type":"session_meta","payload":first_payload}).to_string(),
|
||||
rollout_records(id, &cwd, json!("cli"), "fork copy"),
|
||||
]
|
||||
.join("\n"),
|
||||
)
|
||||
.unwrap();
|
||||
touch(&path, now);
|
||||
}
|
||||
assert!(scan_in_home(root.path(), &cwd, now).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rollout_paths_must_remain_under_approved_roots() {
|
||||
let (_tempdir, root) = canonical_tempdir();
|
||||
let sessions = root.join("sessions");
|
||||
fs::create_dir_all(&sessions).unwrap();
|
||||
let approved_id = uuid::Uuid::from_u128(600);
|
||||
let compressed_id = uuid::Uuid::from_u128(601);
|
||||
let approved = rollout_path(&sessions, approved_id);
|
||||
let compressed_plain = rollout_path(&sessions, compressed_id);
|
||||
let compressed = PathBuf::from(format!("{}.zst", compressed_plain.display()));
|
||||
let outside = rollout_path(&root, uuid::Uuid::from_u128(602));
|
||||
let wrong_extension = sessions.join("not-a-rollout.txt");
|
||||
let directory = rollout_path(&sessions, uuid::Uuid::from_u128(603));
|
||||
fs::write(&approved, "").unwrap();
|
||||
fs::write(&compressed, "").unwrap();
|
||||
fs::write(&outside, "").unwrap();
|
||||
fs::write(&wrong_extension, "").unwrap();
|
||||
fs::create_dir_all(&directory).unwrap();
|
||||
let approved_root = ApprovedRoot::new(&root).unwrap();
|
||||
assert_eq!(
|
||||
existing_rollout_path(
|
||||
&approved_root,
|
||||
&approved.display().to_string(),
|
||||
&approved_id.to_string()
|
||||
),
|
||||
Some(dunce::canonicalize(&approved).unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
existing_rollout_path(
|
||||
&approved_root,
|
||||
&compressed_plain.display().to_string(),
|
||||
&compressed_id.to_string()
|
||||
),
|
||||
Some(dunce::canonicalize(&compressed).unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
existing_rollout_path(
|
||||
&approved_root,
|
||||
&outside.display().to_string(),
|
||||
&uuid::Uuid::from_u128(602).to_string()
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
existing_rollout_path(
|
||||
&approved_root,
|
||||
&wrong_extension.display().to_string(),
|
||||
&uuid::Uuid::from_u128(604).to_string()
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
existing_rollout_path(
|
||||
&approved_root,
|
||||
&directory.display().to_string(),
|
||||
&uuid::Uuid::from_u128(603).to_string()
|
||||
),
|
||||
None
|
||||
);
|
||||
let traversal = format!(
|
||||
"sessions/../{}",
|
||||
outside.file_name().unwrap().to_string_lossy()
|
||||
);
|
||||
assert_eq!(
|
||||
existing_rollout_path(
|
||||
&approved_root,
|
||||
&traversal,
|
||||
&uuid::Uuid::from_u128(602).to_string()
|
||||
),
|
||||
None
|
||||
);
|
||||
let adversarial = sessions.join(format!(
|
||||
"rollout-2027-01-15T12-00-00-extra-{}.jsonl",
|
||||
uuid::Uuid::from_u128(604)
|
||||
));
|
||||
fs::write(&adversarial, "").unwrap();
|
||||
assert_eq!(rollout_id(&adversarial), None);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let link = rollout_path(&sessions, uuid::Uuid::from_u128(602));
|
||||
std::os::unix::fs::symlink(&outside, &link).unwrap();
|
||||
assert_eq!(
|
||||
existing_rollout_path(
|
||||
&approved_root,
|
||||
&link.display().to_string(),
|
||||
&uuid::Uuid::from_u128(602).to_string()
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn fallback_rejects_sessions_parent_symlink_escape() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let outside = TempDir::new().unwrap();
|
||||
let codex_home = root.path().join("codex");
|
||||
let cwd = codex_home.join("repo");
|
||||
fs::create_dir_all(&cwd).unwrap();
|
||||
let now = UNIX_EPOCH + Duration::from_secs(1_800_300_000);
|
||||
let outside_day = session_day(outside.path(), now);
|
||||
fs::create_dir_all(&outside_day).unwrap();
|
||||
let id = uuid::Uuid::from_u128(5_000);
|
||||
let rollout = rollout_path(&outside_day, id);
|
||||
fs::write(
|
||||
&rollout,
|
||||
rollout_records(id, &cwd, json!("cli"), "outside sessions root"),
|
||||
)
|
||||
.unwrap();
|
||||
touch(&rollout, now);
|
||||
std::os::unix::fs::symlink(outside.path().join("sessions"), codex_home.join("sessions"))
|
||||
.unwrap();
|
||||
assert!(scan_in_home(&codex_home, &cwd, now).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_dirs_include_utc_and_offset_boundary_days() {
|
||||
let root = Path::new("/sessions");
|
||||
let now = system_time_from_millis(
|
||||
chrono::DateTime::parse_from_rfc3339("2026-01-01T00:30:00Z")
|
||||
.unwrap()
|
||||
.timestamp_millis(),
|
||||
)
|
||||
.unwrap();
|
||||
let negative = super::super::files::recent_date_dirs(root, now, -2 * 60 * 60);
|
||||
assert_eq!(negative.len(), 32);
|
||||
assert!(negative.contains(&root.join("2026/01/01")));
|
||||
assert!(negative.contains(&root.join("2025/12/31")));
|
||||
let late = system_time_from_millis(
|
||||
chrono::DateTime::parse_from_rfc3339("2026-01-01T23:30:00Z")
|
||||
.unwrap()
|
||||
.timestamp_millis(),
|
||||
)
|
||||
.unwrap();
|
||||
let positive = super::super::files::recent_date_dirs(root, late, 2 * 60 * 60);
|
||||
assert_eq!(positive[0], root.join("2026/01/02"));
|
||||
assert!(positive.contains(&root.join("2026/01/01")));
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use filetime::FileTime;
|
||||
use rusqlite::{Connection, params, types::Value as SqlValue};
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
use crate::foreign_sessions::{
|
||||
canonical_tempdir, millis_from_system_time, system_time_from_millis,
|
||||
};
|
||||
|
||||
mod db;
|
||||
mod files;
|
||||
|
||||
struct DbRow<'a> {
|
||||
id: uuid::Uuid,
|
||||
rollout_path: &'a Path,
|
||||
updated_at_ms: i64,
|
||||
source: &'a str,
|
||||
cwd: &'a Path,
|
||||
title: &'a str,
|
||||
first_user_message: &'a str,
|
||||
archived: bool,
|
||||
}
|
||||
|
||||
fn create_db(path: &Path, rows: &[DbRow<'_>]) {
|
||||
let connection = Connection::open(path).unwrap();
|
||||
connection
|
||||
.execute_batch(
|
||||
"CREATE TABLE threads (
|
||||
id TEXT,
|
||||
rollout_path TEXT,
|
||||
updated_at_ms INTEGER,
|
||||
source TEXT,
|
||||
cwd TEXT,
|
||||
title TEXT,
|
||||
first_user_message TEXT,
|
||||
archived INTEGER,
|
||||
git_branch TEXT
|
||||
);
|
||||
CREATE INDEX threads_archived_cwd_updated \
|
||||
ON threads(archived, cwd, updated_at_ms DESC);",
|
||||
)
|
||||
.unwrap();
|
||||
for row in rows {
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO threads VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, NULL)",
|
||||
params![
|
||||
row.id.to_string(),
|
||||
row.rollout_path.display().to_string(),
|
||||
row.updated_at_ms,
|
||||
row.source,
|
||||
row.cwd.display().to_string(),
|
||||
row.title,
|
||||
row.first_user_message,
|
||||
row.archived,
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn touch(path: &Path, time: SystemTime) {
|
||||
filetime::set_file_mtime(path, FileTime::from_system_time(time)).unwrap();
|
||||
}
|
||||
|
||||
fn session_day(root: &Path, now: SystemTime) -> PathBuf {
|
||||
use chrono::{DateTime, Datelike, Utc};
|
||||
|
||||
let date = DateTime::<Utc>::from(now).date_naive();
|
||||
root.join("sessions")
|
||||
.join(format!("{:04}", date.year()))
|
||||
.join(format!("{:02}", date.month()))
|
||||
.join(format!("{:02}", date.day()))
|
||||
}
|
||||
|
||||
fn rollout_path(dir: &Path, id: uuid::Uuid) -> PathBuf {
|
||||
dir.join(format!("rollout-2027-01-15T12-00-00-{id}.jsonl"))
|
||||
}
|
||||
|
||||
fn write_recent_rollout(
|
||||
root: &Path,
|
||||
cwd: &Path,
|
||||
now: SystemTime,
|
||||
id: uuid::Uuid,
|
||||
source: serde_json::Value,
|
||||
) -> PathBuf {
|
||||
let day = session_day(root, now);
|
||||
fs::create_dir_all(&day).unwrap();
|
||||
let path = rollout_path(&day, id);
|
||||
fs::write(
|
||||
&path,
|
||||
json!({
|
||||
"type": "session_meta",
|
||||
"payload": {
|
||||
"id": id,
|
||||
"cwd": cwd.display().to_string(),
|
||||
"source": source,
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
touch(&path, now);
|
||||
path
|
||||
}
|
||||
|
||||
fn oversized_text(limit: usize, utf8: bool) -> String {
|
||||
if utf8 {
|
||||
"é".repeat(limit / 2 + 1)
|
||||
} else {
|
||||
"x".repeat(limit + 1)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user