Release Batch D: terminal resource-leak fix + fsync atomic writers (#5835, #5843) (#5848)

Phase 2 backend reliability/data-integrity batch. Each byte-verified vs PR head,
Codex SAFE, 20 own + 355 regression tests pass, disjoint files.

- #5835 (ai-ag2026): stop leaking fd + reader thread + shell on client/shell exit (#4633 family)
- #5843 (ai-ag2026): fsync atomic JSON writers (oauth/passkeys/discoverability) + thread-safe temp

Co-authored-by: t <a@b>
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-07-09 10:54:06 -07:00
1 parent 7a1a122e6e
commit d6e5dc302d
8 files changed
+474 -17

No files matched your search

+23 -4
View File
@@ -13,6 +13,7 @@ from __future__ import annotations
import argparse
import json
import os
import threading
import shutil
import sqlite3
from collections import Counter
@@ -387,9 +388,27 @@ def audit_session_discoverability(
def _atomic_write_json(path: Path, payload) -> None:
tmp = path.with_suffix(path.suffix + f".tmp.{os.getpid()}")
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
os.replace(tmp, path)
# Temp name includes the thread id, not just the pid: WebUI runs a
# ThreadingHTTPServer, so two request threads reconciling the same _index.json
# would otherwise collide on one `<name>.tmp.<pid>` path and truncate each
# other's temp before its os.replace. fsync before the rename so a power loss
# can't leave a zero-length/garbage file where the durable JSON should be; a
# finally drops the temp if anything fails. Mode matches the prior write_text
# (umask default) since the plain open() respects the umask.
text = json.dumps(payload, ensure_ascii=False, indent=2)
tmp = path.with_name(f".{path.name}.{os.getpid()}.{threading.get_ident()}.tmp")
try:
with open(tmp, "w", encoding="utf-8") as f:
f.write(text)
f.flush()
os.fsync(f.fileno())
os.replace(tmp, path)
except BaseException:
try:
os.unlink(tmp)
except OSError:
pass
raise
def _backup_file(path: Path, backup_dir: Path, backed_up: dict[Path, str]) -> str | None:
@@ -482,7 +501,7 @@ def _materialize_sidecar_from_state_db(session_dir: Path, state_db_path: Path |
payload = _state_db_row_to_sidecar(row)
_backup_file(state_db_path, backup_dir, backed_up)
session_dir.mkdir(parents=True, exist_ok=True)
tmp = target.with_suffix(target.suffix + f".tmp.{os.getpid()}")
tmp = target.with_suffix(target.suffix + f".tmp.{os.getpid()}.{threading.get_ident()}")
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
try:
os.link(str(tmp), str(target))