88import sqlite3
99from dataclasses import dataclass
1010from datetime import datetime
11- from typing import Any , Literal , TypedDict
11+ from typing import Any , Literal
1212
1313from models import Bubble
14+ from models .export import CollectedExportEntry
1415from services .summary_cache import fingerprint_workspace_storage , nocache_enabled
1516from services .workspace_context import (
1617 WorkspaceContext ,
4950SinceMode = Literal ["all" , "last" ]
5051
5152
52- class ExportEntry (TypedDict ):
53- """One exportable conversation with rendered markdown."""
54-
55- id : str
56- rel_path : str
57- content : str
58- out_path : str
59- updatedAt : int
60- title : str
61- workspace : str
53+ def read_last_export_ms (
54+ since : SinceMode ,
55+ * ,
56+ state_path : str | None = None ,
57+ state : dict [str , Any ] | None = None ,
58+ ) -> int :
59+ """Return last-export epoch ms for ``since=last``; 0 for a full export."""
60+ if since != "last" :
61+ return 0
62+ ts : Any = None
63+ if state is not None :
64+ ts = state .get ("lastExportTime" )
65+ elif state_path is not None and os .path .isfile (state_path ):
66+ try :
67+ with open (state_path , "r" , encoding = "utf-8" ) as f :
68+ st = json .load (f )
69+ if isinstance (st , dict ):
70+ ts = st .get ("lastExportTime" )
71+ except (json .JSONDecodeError , ValueError , OSError ) as e :
72+ _logger .warning (
73+ "Could not read last export timestamp; defaulting to full export: %s" ,
74+ e ,
75+ )
76+ if ts :
77+ return to_epoch_ms (ts )
78+ return 0
6279
6380
6481@dataclass (frozen = True )
@@ -209,9 +226,9 @@ def _collect_ide_export_entries(
209226 last_export_ms : int ,
210227 today : str ,
211228 out_dir : str ,
212- ) -> list [ExportEntry ]:
229+ ) -> list [CollectedExportEntry ]:
213230 ctx = orch .ctx
214- exported : list [ExportEntry ] = []
231+ exported : list [CollectedExportEntry ] = []
215232 for row in db_data .ide_composer_rows :
216233 composer_id = row ["key" ].split (":" )[1 ]
217234 try :
@@ -239,6 +256,8 @@ def _collect_ide_export_entries(
239256 updated_at = to_epoch_ms (cd .get ("lastUpdatedAt" )) or to_epoch_ms (
240257 cd .get ("createdAt" ),
241258 )
259+ # Intentional behavior change vs legacy CLI: fall back to createdAt when
260+ # lastUpdatedAt is absent (affects timestamps, filenames, and --since last).
242261 if since == "last" and updated_at <= last_export_ms :
243262 continue
244263
@@ -325,7 +344,7 @@ def _collect_ide_export_entries(
325344 workspace_info = {"ws_slug" : ws_slug , "ws_display_name" : ws_display_name },
326345 )
327346
328- rel_path = os .path .join ( today , ws_slug , "chat" , filename )
347+ rel_path = os .path .relpath ( out_path , out_dir )
329348 exported .append ({
330349 "id" : composer_id ,
331350 "rel_path" : rel_path ,
@@ -345,8 +364,8 @@ def _collect_cli_export_entries(
345364 last_export_ms : int ,
346365 today : str ,
347366 out_dir : str ,
348- ) -> list [ExportEntry ]:
349- exported : list [ExportEntry ] = []
367+ ) -> list [CollectedExportEntry ]:
368+ exported : list [CollectedExportEntry ] = []
350369 try :
351370 cli_projects = list_cli_projects (get_cli_chats_path ())
352371 except Exception as e : # noqa: BLE001 — log and skip CLI enumeration on any failure
@@ -446,7 +465,7 @@ def _collect_cli_export_entries(
446465 bubbles = bubbles ,
447466 title_override = title ,
448467 )
449- rel_path = os .path .join ( today , ws_slug_cli , "cli" , filename )
468+ rel_path = os .path .relpath ( out_path , out_dir )
450469 exported .append ({
451470 "id" : session_id ,
452471 "rel_path" : rel_path ,
@@ -469,14 +488,14 @@ def collect_export_entries(
469488 include_composer : bool = True ,
470489 include_cli : bool = True ,
471490 nocache : bool = False ,
472- ) -> list [ExportEntry ]:
491+ ) -> list [CollectedExportEntry ]:
473492 """Collect exportable conversations (IDE + CLI) via shared orchestration."""
474493 effective_nocache = nocache_enabled (request_nocache = nocache )
475494 orch = prepare_workspace_orchestration (
476495 workspace_path , exclusion_rules , nocache = effective_nocache ,
477496 )
478497 today = datetime .now ().strftime ("%Y-%m-%d" )
479- exported : list [ExportEntry ] = []
498+ exported : list [CollectedExportEntry ] = []
480499
481500 if include_composer :
482501 db_data = load_global_db_export_data (orch )
0 commit comments