Skip to content

Commit d75620d

Browse files
Harden export orchestration from review feedback
Use to_epoch_ms for lastExportTime parsing, validate composerData shape, serialize CLI tool-call fields safely, and pass effective nocache flag through workspace listing orchestration.
1 parent 2b76dd5 commit d75620d

3 files changed

Lines changed: 26 additions & 12 deletions

File tree

scripts/export.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,7 @@ def _read_last_export_ms(state_path: str, since: Literal["all", "last"]) -> int:
176176
st = json.load(f)
177177
ts = st.get("lastExportTime")
178178
if ts:
179-
return int(
180-
datetime.fromisoformat(ts.replace("Z", "+00:00")).timestamp() * 1000,
181-
)
179+
return to_epoch_ms(ts)
182180
except (json.JSONDecodeError, ValueError, OSError) as e:
183181
_logger.warning(
184182
"Could not read last export timestamp; defaulting to full export: %s",

services/export_engine.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def json_dump_safe(value: object) -> str:
8888
"""Best-effort JSON serialization for exclusion matching."""
8989
try:
9090
return json.dumps(value, ensure_ascii=False, sort_keys=True)
91-
except Exception:
91+
except Exception: # noqa: BLE001 — best-effort fallback when value is not JSON-serializable
9292
return str(value) if value is not None else ""
9393

9494

@@ -224,8 +224,16 @@ def _collect_ide_export_entries(
224224
)
225225
continue
226226

227+
if not isinstance(cd, dict):
228+
_logger.debug(
229+
"Skipping corrupt composerData row %s: expected object, got %s",
230+
composer_id,
231+
type(cd).__name__,
232+
)
233+
continue
234+
227235
headers = cd.get("fullConversationHeadersOnly") or []
228-
if not headers:
236+
if not isinstance(headers, list) or not headers:
229237
continue
230238

231239
updated_at = to_epoch_ms(cd.get("lastUpdatedAt"))
@@ -270,7 +278,12 @@ def _collect_ide_export_entries(
270278
bubble_texts: list[str] = []
271279
bubble_meta_parts: list[str] = []
272280
for h in headers:
273-
b = db_data.bubble_map.get(h.get("bubbleId"))
281+
if not isinstance(h, dict):
282+
continue
283+
bubble_id = h.get("bubbleId")
284+
if not isinstance(bubble_id, str):
285+
continue
286+
b = db_data.bubble_map.get(bubble_id)
274287
if not b:
275288
continue
276289
text = extract_text_from_bubble(b)
@@ -337,7 +350,7 @@ def _collect_cli_export_entries(
337350
exported: list[ExportEntry] = []
338351
try:
339352
cli_projects = list_cli_projects(get_cli_chats_path())
340-
except Exception as e:
353+
except Exception as e: # noqa: BLE001 — log and skip CLI enumeration on any failure
341354
_logger.warning(
342355
"Could not enumerate CLI chats: %s (%s) — skipping",
343356
e,
@@ -375,7 +388,7 @@ def _collect_cli_export_entries(
375388
try:
376389
messages = traverse_blobs(session["db_path"])
377390
bubbles = messages_to_bubbles(messages, created_ms)
378-
except Exception as e:
391+
except Exception as e: # noqa: BLE001 — log and skip session on read/parse failure
379392
_logger.warning(
380393
"Could not read CLI session %s: %s (%s)",
381394
session_id,
@@ -403,7 +416,7 @@ def _collect_cli_export_entries(
403416

404417
bubble_texts = [b["text"] for b in bubbles if b.get("text")]
405418
tool_call_texts = [
406-
tc.get("input", "") or tc.get("summary", "")
419+
json_dump_safe(tc.get("input", "") or tc.get("summary", ""))
407420
for b in bubbles
408421
for tc in (b.get("metadata") or {}).get("toolCalls") or []
409422
]

services/workspace_listing.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,19 @@ def list_workspace_projects(
9090
parse-error dicts (``type``, ``count``, ``detail``) from
9191
:meth:`models.ParseWarningCollector.to_api_list`; empty when no skips.
9292
"""
93-
orch = prepare_workspace_orchestration(workspace_path, rules, nocache=nocache)
94-
if not nocache_enabled(request_nocache=nocache):
93+
effective_nocache = nocache_enabled(request_nocache=nocache)
94+
orch = prepare_workspace_orchestration(
95+
workspace_path, rules, nocache=effective_nocache,
96+
)
97+
if not effective_nocache:
9598
cached = get_cached_projects(orch.fingerprint)
9699
if cached is not None:
97100
return cached
98101

99102
projects, warnings = _build_workspace_projects_uncached(
100103
workspace_path, rules, orch,
101104
)
102-
if not nocache_enabled(request_nocache=nocache):
105+
if not effective_nocache:
103106
set_cached_projects(orch.fingerprint, projects, warnings)
104107
return projects, warnings
105108

0 commit comments

Comments
 (0)