{"record":{"id":"c185701a7645473a","repo":"laurent22/joplin","slug":"processingpathtwice","errorCode":"processingPathTwice","errorMessage":"Processing a path that has already been done: %s. Remote item has an updated_time in the future","messagePattern":"Processing a path that has already been done: (.+?)\\. Remote item has an updated_time in the future","errorType":"exception","errorClass":"JoplinError","httpStatus":null,"severity":"warning","filePath":"packages/lib/Synchronizer.ts","lineNumber":654,"sourceCode":"\t\t\t\t\t\tconst path = BaseItem.systemPath(local);\n\n\t\t\t\t\t\t// Safety check to avoid infinite loops.\n\t\t\t\t\t\t// - In fact this error is possible if the item is marked for sync (via sync_time or force_sync) while synchronisation is in\n\t\t\t\t\t\t//   progress. When force_sync is not true, this is because the user is typing while the sync is running, so we should continue\n\t\t\t\t\t\t//   looping, as we don't want the sync to stop when there are still un-synced outgoing changes, otherwise this creates a race condition\n\t\t\t\t\t\t//   on mobile, where additional changes made during upload are not synced and don't trigger another sync, whereas a change made immediately\n\t\t\t\t\t\t//   after the sync has finished will trigger another sync. Once the user has stopped typing, it can then break out of the loop and continue\n\t\t\t\t\t\t//   the rest of the process.\n\t\t\t\t\t\t// - It can also happen if the item is directly modified in the sync target, and set with an update_time in the future. In that case,\n\t\t\t\t\t\t//   the local sync_time will be updated to Date.now() but on the next loop it will see that the remote item still has a date ahead\n\t\t\t\t\t\t//   and will see a conflict. There's currently no automatic fix for this - the remote item on the sync target must be fixed manually\n\t\t\t\t\t\t//   (by setting an updated_time less than current time).\n\t\t\t\t\t\tif (donePaths.indexOf(path) >= 0) {\n\t\t\t\t\t\t\tconst syncItem = await BaseItem.syncItem(syncTargetId, local.id, { fields: ['force_sync'] });\n\t\t\t\t\t\t\tif (local.updated_time > time.unixMs() + Day) {\n\t\t\t\t\t\t\t\tthrow new Error(sprintf('Remote item %s has an updated_time in the future', path));\n\t\t\t\t\t\t\t} else if (local.updated_time > time.unixMs()) {\n\t\t\t\t\t\t\t\tthrow new JoplinError(sprintf('Processing a path that has already been done: %s. Remote item has an updated_time in the future', path), 'processingPathTwice');\n\t\t\t\t\t\t\t} else if (syncItem.force_sync) {\n\t\t\t\t\t\t\t\tthrow new JoplinError(sprintf('Processing a path that has already been done: %s. Item was marked for sync using force_sync', path), 'processingPathTwice');\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tthrow new JoplinError(sprintf('Processing a path that has already been done: %s. The user is making changes while the sync is in progress', path), 'changedDuringSync');\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst remote: RemoteItem = result.neverSyncedItemIds.includes(local.id) ? null : await this.apiCall('stat', path);\n\t\t\t\t\t\tlet action: SyncAction = null;\n\t\t\t\t\t\tlet itemIsReadOnly = false;\n\t\t\t\t\t\tlet reason = '';\n\t\t\t\t\t\tlet remoteContent = null;\n\n\t\t\t\t\t\tconst getConflictType = (conflictedItem: { type_?: number }) => {\n\t\t\t\t\t\t\tif (conflictedItem.type_ === BaseModel.TYPE_NOTE) return SyncAction.NoteConflict;\n\t\t\t\t\t\t\tif (conflictedItem.type_ === BaseModel.TYPE_RESOURCE) return SyncAction.ResourceConflict;\n\t\t\t\t\t\t\treturn SyncAction.ItemConflict;\n\t\t\t\t\t\t};","sourceCodeStart":636,"sourceCodeEnd":672,"githubUrl":"https://github.com/laurent22/joplin/blob/2654b33620775080d1d59c552259d41e33dad3d2/packages/lib/Synchronizer.ts#L636-L672","documentation":"A JoplinError (code 'processingPathTwice') thrown in the DELTA loop when a path is encountered again in donePaths AND local.updated_time is in the future but within one Day of now. Unlike error 142 (more than a day ahead), this is treated as a reprocessing safety trip — the item's timestamp is slightly ahead of the clock, so the loop would otherwise repeat indefinitely.","triggerScenarios":"A path is processed, added to donePaths, then appears again in the same loop because local.updated_time is between time.unixMs() and time.unixMs()+Day — small clock skew or a just-saved item whose timestamp rounds up.","commonSituations":"Minor clock skew between the client and sync target; an item saved with a timestamp a few seconds/minutes in the future; OneDrive-style targets whose lastModifiedDateTime is reported slightly ahead.","solutions":["Let the current sync finish; the next sync cycle usually clears it once wall time catches up.","Verify the system clock is accurate (NTP sync) on all syncing devices.","If persistent, manually correct the affected item's updated_time to a past value on the sync target.","Avoid editing items directly on the sync target while a sync is running."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Check for small clock skew before syncing\nconst now = Date.now();\nconst slightlyAhead = items.filter(\n  i => i.updated_time > now && i.updated_time <= now + Day,\n);\nif (slightlyAhead.length) {\n  // wait until wall time catches up, or clamp the timestamp\n}","typeGuard":"function isProcessingPathTwice(e: unknown): e is JoplinError {\n  return e instanceof JoplinError && (e as any).code === 'processingPathTwice';\n}","tryCatchPattern":"try {\n  await synchronizer.start(options);\n} catch (error) {\n  if (isProcessingPathTwice(error)) {\n    // benign clock-skew; retry on the next sync cycle\n    scheduleRetry();\n    return;\n  }\n  throw error;\n}","preventionTips":["Ensure NTP time sync on all clients.","Avoid editing items directly on the sync target during active sync.","Treat 'processingPathTwice' as transient unless it persists across multiple cycles."],"tags":["sync","timestamp","conflict","clock-skew"],"backgroundTag":null,"analyzedSha":"2654b33620775080d1d59c552259d41e33dad3d2","analyzedAt":"2026-08-12T14:26:46.263Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}