@@ -88,20 +88,75 @@ export function calculateChecksum(data: string): string {
8888
8989/**
9090 * Generate filename for an item in storage.
91+ *
92+ * Item IDs come from category-loader.ts in format: {type}/{parent}/{file}.json
93+ * We transform these to a hierarchical structure for remote storage:
94+ * - Sessions: session/{projectHash}/{sessionId}.json → sessions/{projectHash}/{sessionId}.json.gz
95+ * - Messages: message/{sessionId}/{messageId}.json → messages/{sessionId}/{messageId}.json.gz
96+ * - Parts: part/{messageId}/{partId}.json → messages/parts/{messageId}/{partId}.json.gz
9197 */
9298export function getItemFilename ( category : SyncCategory , itemId : string ) : string {
93- // Sanitize item ID for filename (replace unsafe chars)
94- const safeId = itemId . replace ( / [ / \\ : * ? " < > | ] / g, '_' ) ;
99+ if ( category === 'sessions' ) {
100+ // itemId: session/{projectHash}/{sessionId}.json
101+ // Output: sessions/{projectHash}/{sessionId}.json.gz
102+ const match = / ^ s e s s i o n \/ ( [ ^ / ] + ) \/ ( .+ ) \. j s o n $ / . exec ( itemId ) ;
103+ if ( match ?. [ 1 ] && match [ 2 ] ) {
104+ return `sessions/${ match [ 1 ] } /${ match [ 2 ] } .json.gz` ;
105+ }
106+ }
107+
108+ if ( category === 'messages' ) {
109+ // Message: message/{sessionId}/{messageId}.json
110+ // Output: messages/{sessionId}/{messageId}.json.gz
111+ const msgMatch = / ^ m e s s a g e \/ ( s e s _ [ ^ / ] + ) \/ ( .+ ) \. j s o n $ / . exec ( itemId ) ;
112+ if ( msgMatch ?. [ 1 ] && msgMatch [ 2 ] ) {
113+ return `messages/${ msgMatch [ 1 ] } /${ msgMatch [ 2 ] } .json.gz` ;
114+ }
115+
116+ // Part: part/{messageId}/{partId}.json
117+ // Output: messages/parts/{messageId}/{partId}.json.gz
118+ const partMatch = / ^ p a r t \/ ( m s g _ [ ^ / ] + ) \/ ( .+ ) \. j s o n $ / . exec ( itemId ) ;
119+ if ( partMatch ?. [ 1 ] && partMatch [ 2 ] ) {
120+ return `messages/parts/${ partMatch [ 1 ] } /${ partMatch [ 2 ] } .json.gz` ;
121+ }
122+ }
123+
124+ // Fallback: flat structure (replace path separators and unsafe chars)
125+ const safeId = itemId . replace ( / [ / \\ : * ? " < > | ] / g, '_' ) . replace ( / \. j s o n $ / , '' ) ;
95126 return `${ category } /${ safeId } .json.gz` ;
96127}
97128
98129/**
99130 * Extract item ID from filename.
131+ *
132+ * Reverses the hierarchical remote path back to the original item ID format
133+ * used by category-loader.ts.
100134 */
101135export function getItemIdFromFilename ( filename : string ) : string | null {
102- const regex = / ^ [ ^ / ] + \/ ( .+ ) \. j s o n \. g z $ / ;
103- const match = regex . exec ( filename ) ;
104- return match ? ( match [ 1 ] ?? null ) : null ;
136+ // Sessions: sessions/{projectHash}/{sessionId}.json.gz -> session/{projectHash}/{sessionId}.json
137+ const sessionMatch = / ^ s e s s i o n s \/ ( [ ^ / ] + ) \/ ( [ ^ / ] + ) \. j s o n \. g z $ / . exec ( filename ) ;
138+ if ( sessionMatch ?. [ 1 ] && sessionMatch [ 2 ] ) {
139+ return `session/${ sessionMatch [ 1 ] } /${ sessionMatch [ 2 ] } .json` ;
140+ }
141+
142+ // Messages: messages/{sessionId}/{messageId}.json.gz -> message/{sessionId}/{messageId}.json
143+ const msgMatch = / ^ m e s s a g e s \/ ( s e s _ [ ^ / ] + ) \/ ( [ ^ / ] + ) \. j s o n \. g z $ / . exec ( filename ) ;
144+ if ( msgMatch ?. [ 1 ] && msgMatch [ 2 ] ) {
145+ return `message/${ msgMatch [ 1 ] } /${ msgMatch [ 2 ] } .json` ;
146+ }
147+
148+ // Parts: messages/parts/{messageId}/{partId}.json.gz -> part/{messageId}/{partId}.json
149+ const partMatch = / ^ m e s s a g e s \/ p a r t s \/ ( m s g _ [ ^ / ] + ) \/ ( [ ^ / ] + ) \. j s o n \. g z $ / . exec ( filename ) ;
150+ if ( partMatch ?. [ 1 ] && partMatch [ 2 ] ) {
151+ return `part/${ partMatch [ 1 ] } /${ partMatch [ 2 ] } .json` ;
152+ }
153+
154+ // Fallback: old flat format - restore slashes from underscores and add .json
155+ const fallbackMatch = / ^ [ ^ / ] + \/ ( .+ ) \. j s o n \. g z $ / . exec ( filename ) ;
156+ if ( fallbackMatch ?. [ 1 ] ) {
157+ return fallbackMatch [ 1 ] . replace ( / _ / g, '/' ) + '.json' ;
158+ }
159+ return null ;
105160}
106161
107162/**
0 commit comments