44import os
55import pathlib
66import shutil
7+ import signal
78import sqlite3
89import subprocess
910import sys
1920 rollout_path TEXT NOT NULL,
2021 created_at INTEGER NOT NULL,
2122 updated_at INTEGER NOT NULL,
23+ created_at_ms INTEGER,
24+ updated_at_ms INTEGER,
2225 source TEXT NOT NULL,
26+ thread_source TEXT,
2327 model_provider TEXT NOT NULL,
2428 cwd TEXT NOT NULL,
2529 title TEXT NOT NULL,
3943 memory_mode TEXT NOT NULL DEFAULT 'enabled',
4044 model TEXT,
4145 reasoning_effort TEXT,
42- agent_path TEXT
46+ agent_path TEXT,
47+ preview TEXT NOT NULL DEFAULT ''
4348);
49+ CREATE INDEX idx_threads_created_at ON threads(created_at DESC, id DESC);
50+ CREATE INDEX idx_threads_updated_at ON threads(updated_at DESC, id DESC);
51+ CREATE INDEX idx_threads_archived ON threads(archived);
52+ CREATE INDEX idx_threads_source ON threads(source);
53+ CREATE INDEX idx_threads_provider ON threads(model_provider);
54+ CREATE INDEX idx_threads_created_at_ms ON threads(created_at_ms DESC, id DESC);
55+ CREATE INDEX idx_threads_updated_at_ms ON threads(updated_at_ms DESC, id DESC);
56+ CREATE INDEX idx_threads_archived_cwd_created_at_ms
57+ ON threads(archived, cwd, created_at_ms DESC, id DESC);
58+ CREATE INDEX idx_threads_archived_cwd_updated_at_ms
59+ ON threads(archived, cwd, updated_at_ms DESC, id DESC);
60+ CREATE TRIGGER threads_created_at_ms_after_insert
61+ AFTER INSERT ON threads
62+ WHEN NEW.created_at_ms IS NULL
63+ BEGIN
64+ UPDATE threads
65+ SET created_at_ms = NEW.created_at * 1000
66+ WHERE id = NEW.id;
67+ END;
68+ CREATE TRIGGER threads_updated_at_ms_after_insert
69+ AFTER INSERT ON threads
70+ WHEN NEW.updated_at_ms IS NULL
71+ BEGIN
72+ UPDATE threads
73+ SET updated_at_ms = NEW.updated_at * 1000
74+ WHERE id = NEW.id;
75+ END;
76+ CREATE TRIGGER threads_created_at_ms_after_update
77+ AFTER UPDATE OF created_at ON threads
78+ WHEN NEW.created_at != OLD.created_at
79+ AND NEW.created_at_ms IS OLD.created_at_ms
80+ BEGIN
81+ UPDATE threads
82+ SET created_at_ms = NEW.created_at * 1000
83+ WHERE id = NEW.id;
84+ END;
85+ CREATE TRIGGER threads_updated_at_ms_after_update
86+ AFTER UPDATE OF updated_at ON threads
87+ WHEN NEW.updated_at != OLD.updated_at
88+ AND NEW.updated_at_ms IS OLD.updated_at_ms
89+ BEGIN
90+ UPDATE threads
91+ SET updated_at_ms = NEW.updated_at * 1000
92+ WHERE id = NEW.id;
93+ END;
4494"""
4595
4696COMMAND_TIMEOUT_SECONDS = 30
@@ -76,6 +126,9 @@ def main() -> int:
76126 print (f"[smoke] sync passed with backup: { backup_path } " )
77127 assert_backup_contains_dirty_rows (backup_path )
78128 print ("[smoke] backup verification passed" )
129+ assert_rollout_provider (codex_home , "rollout-a.jsonl" , "1" , "openai" )
130+ assert_rollout_provider (codex_home , "rollout-b.jsonl" , "2" , "openai" )
131+ print ("[smoke] rollout metadata verification passed" )
79132 assert_status (binary_path , codex_home , expected_total = 3 , expected_mismatched = 0 )
80133 print ("[smoke] post-sync status passed" )
81134 run_service_install (binary_path , codex_home )
@@ -85,10 +138,11 @@ def main() -> int:
85138 binary_path , codex_home , expected_installed = "yes" , expected_running = "yes"
86139 )
87140 print ("[smoke] service reached installed=yes running=yes" )
88- insert_dirty_row ( codex_home / "state_5.sqlite" )
89- write_new_session (codex_home )
141+ rollout_path = write_new_session ( codex_home )
142+ insert_dirty_row (codex_home / "state_5.sqlite" , rollout_path )
90143 print ("[smoke] inserted dirty row and wrote new session" )
91144 wait_for_reconcile (binary_path , codex_home )
145+ assert_rollout_provider (codex_home , "rollout-d.jsonl" , "4" , "openai" )
92146 print ("[smoke] background reconcile passed" )
93147 finally :
94148 run_service_uninstall (binary_path , codex_home )
@@ -126,35 +180,98 @@ def prepare_codex_home(codex_home: pathlib.Path) -> None:
126180
127181 sessions_dir = codex_home / "sessions" / "2026" / "04" / "15"
128182 sessions_dir .mkdir (parents = True , exist_ok = True )
129- (sessions_dir / "rollout-a.jsonl" ).write_text (
130- '{"type":"session_meta","payload":{"model_provider":"vm"}}\n ' ,
131- encoding = "utf-8" ,
132- )
133- (sessions_dir / "rollout-b.jsonl" ).write_text (
134- '{"type":"session_meta","payload":{"model_provider":"cp"}}\n ' ,
135- encoding = "utf-8" ,
136- )
183+ rollout_a = sessions_dir / "rollout-a.jsonl"
184+ rollout_b = sessions_dir / "rollout-b.jsonl"
185+ rollout_c = sessions_dir / "rollout-c.jsonl"
186+ write_rollout (rollout_a , "1" , "vm" )
187+ write_rollout (rollout_b , "2" , "cp" )
188+ write_rollout (rollout_c , "3" , "openai" )
137189
138190 sqlite_path = codex_home / "state_5.sqlite"
139191 connection = sqlite3 .connect (sqlite_path )
140192 connection .executescript (SCHEMA_SQL )
141193 connection .executemany (
142194 """
143195 INSERT INTO threads (
144- id, rollout_path, created_at, updated_at, source, model_provider, cwd, title,
145- sandbox_policy, approval_mode
146- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
196+ id, rollout_path, created_at, updated_at, created_at_ms, updated_at_ms,
197+ source, thread_source, model_provider, cwd, title, sandbox_policy, approval_mode,
198+ cli_version, model, reasoning_effort, first_user_message, preview
199+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
147200 """ ,
148201 [
149- ("1" , "/tmp/a" , 1 , 1 , "cli" , "vm" , "/tmp" , "a" , "workspace-write" , "auto" ),
150- ("2" , "/tmp/b" , 1 , 1 , "cli" , "cp" , "/tmp" , "b" , "workspace-write" , "auto" ),
151- ("3" , "/tmp/c" , 1 , 1 , "cli" , "openai" , "/tmp" , "c" , "workspace-write" , "auto" ),
202+ (
203+ "1" ,
204+ str (rollout_a ),
205+ 1 ,
206+ 1 ,
207+ 1000 ,
208+ 1000 ,
209+ "cli" ,
210+ "user" ,
211+ "vm" ,
212+ "/tmp" ,
213+ "a" ,
214+ "workspace-write" ,
215+ "auto" ,
216+ "0.0.0-test" ,
217+ "gpt-5-codex" ,
218+ "medium" ,
219+ "a" ,
220+ "a" ,
221+ ),
222+ (
223+ "2" ,
224+ str (rollout_b ),
225+ 1 ,
226+ 1 ,
227+ 1001 ,
228+ 1001 ,
229+ "cli" ,
230+ "user" ,
231+ "cp" ,
232+ "/tmp" ,
233+ "b" ,
234+ "workspace-write" ,
235+ "auto" ,
236+ "0.0.0-test" ,
237+ "gpt-5-codex" ,
238+ "medium" ,
239+ "b" ,
240+ "b" ,
241+ ),
242+ (
243+ "3" ,
244+ str (rollout_c ),
245+ 1 ,
246+ 1 ,
247+ 1002 ,
248+ 1002 ,
249+ "cli" ,
250+ "user" ,
251+ "openai" ,
252+ "/tmp" ,
253+ "c" ,
254+ "workspace-write" ,
255+ "auto" ,
256+ "0.0.0-test" ,
257+ "gpt-5-codex" ,
258+ "medium" ,
259+ "c" ,
260+ "c" ,
261+ ),
152262 ],
153263 )
154264 connection .commit ()
155265 connection .close ()
156266
157267
268+ def write_rollout (path : pathlib .Path , thread_id : str , provider : str ) -> None :
269+ path .write_text (
270+ f'{{"type":"session_meta","payload":{{"id":"{ thread_id } ","model_provider":"{ provider } "}}}}\n ' ,
271+ encoding = "utf-8" ,
272+ )
273+
274+
158275def command_env () -> dict [str , str ]:
159276 env = os .environ .copy ()
160277 env ["CODEX_THREADRIPPER_LANG" ] = "en"
@@ -213,6 +330,8 @@ def run_sync(binary_path: pathlib.Path, codex_home: pathlib.Path) -> pathlib.Pat
213330 output = run_command (binary_path , codex_home , "sync" )
214331 if "Rows updated: 2" not in output :
215332 raise RuntimeError (f"unexpected sync output\n \n { output } " )
333+ if "Rollouts updated: 2" not in output :
334+ raise RuntimeError (f"sync did not rewrite rollout metadata\n \n { output } " )
216335
217336 backup_line = next (
218337 (line for line in output .splitlines () if line .startswith ("Backup: " )),
@@ -223,9 +342,34 @@ def run_sync(binary_path: pathlib.Path, codex_home: pathlib.Path) -> pathlib.Pat
223342 backup_path = pathlib .Path (backup_line .removeprefix ("Backup: " ).strip ())
224343 if not backup_path .exists ():
225344 raise RuntimeError (f"backup file is missing: { backup_path } " )
345+ journal_line = next (
346+ (line for line in output .splitlines () if line .startswith ("Rollout first-line journal: " )),
347+ None ,
348+ )
349+ if journal_line is None :
350+ raise RuntimeError (f"missing rollout journal line in sync output\n \n { output } " )
351+ journal_path = pathlib .Path (journal_line .removeprefix ("Rollout first-line journal: " ).strip ())
352+ if not journal_path .exists ():
353+ raise RuntimeError (f"rollout journal file is missing: { journal_path } " )
354+ journal = journal_path .read_text (encoding = "utf-8" )
355+ if '"thread_id":"1"' not in journal or '"thread_id":"2"' not in journal :
356+ raise RuntimeError (f"rollout journal did not include rewritten threads\n \n { journal } " )
226357 return backup_path
227358
228359
360+ def assert_rollout_provider (
361+ codex_home : pathlib .Path , rollout_name : str , thread_id : str , provider : str
362+ ) -> None :
363+ rollout_path = codex_home / "sessions" / "2026" / "04" / "15" / rollout_name
364+ first_line = rollout_path .read_text (encoding = "utf-8" ).splitlines ()[0 ]
365+ expected_id = f'"id":"{ thread_id } "'
366+ expected_provider = f'"model_provider":"{ provider } "'
367+ if expected_id not in first_line or expected_provider not in first_line :
368+ raise RuntimeError (
369+ f"unexpected rollout first line for { rollout_path } \n \n { first_line } "
370+ )
371+
372+
229373def assert_backup_contains_dirty_rows (backup_path : pathlib .Path ) -> None :
230374 connection = sqlite3 .connect (backup_path )
231375 mismatched = connection .execute (
@@ -269,28 +413,47 @@ def wait_for_service_status(
269413 )
270414
271415
272- def insert_dirty_row (sqlite_path : pathlib .Path ) -> None :
416+ def insert_dirty_row (sqlite_path : pathlib .Path , rollout_path : pathlib . Path ) -> None :
273417 connection = sqlite3 .connect (sqlite_path )
274418 connection .execute (
275419 """
276420 INSERT INTO threads (
277- id, rollout_path, created_at, updated_at, source, model_provider, cwd, title,
278- sandbox_policy, approval_mode
279- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
421+ id, rollout_path, created_at, updated_at, created_at_ms, updated_at_ms,
422+ source, thread_source, model_provider, cwd, title, sandbox_policy, approval_mode,
423+ cli_version, model, reasoning_effort, first_user_message, preview
424+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
280425 """ ,
281- ("4" , "/tmp/d" , 1 , 1 , "cli" , "vm" , "/tmp" , "d" , "workspace-write" , "auto" ),
426+ (
427+ "4" ,
428+ str (rollout_path ),
429+ 1 ,
430+ 1 ,
431+ 1003 ,
432+ 1003 ,
433+ "cli" ,
434+ "user" ,
435+ "vm" ,
436+ "/tmp" ,
437+ "d" ,
438+ "workspace-write" ,
439+ "auto" ,
440+ "0.0.0-test" ,
441+ "gpt-5-codex" ,
442+ "medium" ,
443+ "d" ,
444+ "d" ,
445+ ),
282446 )
283447 connection .commit ()
284448 connection .close ()
285449
286450
287- def write_new_session (codex_home : pathlib .Path ) -> None :
451+ def write_new_session (codex_home : pathlib .Path ) -> pathlib . Path :
288452 sessions_dir = codex_home / "sessions" / "2026" / "04" / "15"
289453 sessions_dir .mkdir (parents = True , exist_ok = True )
290- (sessions_dir / "rollout-c.jsonl" ).write_text (
291- '{"type":"session_meta","payload":{"model_provider":"vm"}}\n ' ,
292- encoding = "utf-8" ,
293- )
454+ rollout_path = sessions_dir / "rollout-d.jsonl"
455+ write_rollout (rollout_path , "4" , "vm" )
456+ return rollout_path
294457
295458
296459def wait_for_reconcile (binary_path : pathlib .Path , codex_home : pathlib .Path ) -> None :
0 commit comments